Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/matplotlib/backends/_macosx.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
def get_screens() -> list[tuple[int, float, float, float, float]]: ...

class FigureManager:
def __init__(self, canvas: object, x: float = ..., y: float = ...) -> None: ...
def _show(self) -> None: ...
def _raise(self) -> None: ...
def destroy(self) -> None: ...
def _set_window_mode(self, mode: str) -> None: ...
@staticmethod
def set_icon(filename: str) -> None: ...
def set_window_title(self, title: str) -> None: ...
def get_window_title(self) -> str | None: ...
def resize(self, width: int, height: int) -> None: ...
def full_screen_toggle(self) -> None: ...
def get_window_frame(self) -> tuple[float, float, float, float]: ...
def set_window_frame(self, x: float, y: float, w: float, h: float) -> None: ...
def get_screen_frame(self) -> tuple[float, float, float, float]: ...
def get_window_screen_id(self) -> int: ...
def set_window_level(self, floating: bool) -> None: ...
def get_window_level(self) -> bool: ...
61 changes: 59 additions & 2 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,15 @@ def save_figure(self, *args):
class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
_toolbar2_class = NavigationToolbar2Mac

def __init__(self, canvas, num):
def __init__(self, canvas, num, *, x=None, y=None):
self._shown = False
_macosx.FigureManager.__init__(self, canvas)
self._window_event_callbacks = cbook.CallbackRegistry()
kwargs = {}
if x is not None:
kwargs['x'] = x
if y is not None:
kwargs['y'] = y
_macosx.FigureManager.__init__(self, canvas, **kwargs)
icon_path = str(cbook._get_data_path('images/matplotlib.pdf'))
_macosx.FigureManager.set_icon(icon_path)
FigureManagerBase.__init__(self, canvas, num)
Expand All @@ -165,6 +171,57 @@ def _close_button_pressed(self):
Gcf.destroy(self)
self.canvas.flush_events()

def mpl_connect(self, event_name, callback):
"""Register *callback* to be called on a window event.

Parameters
----------
event_name : str
One of ``'window_resize_event'``, ``'window_resize_end_event'``,
``'window_move_event'``, ``'window_move_end_event'``,
``'focus_in_event'``, ``'focus_out_event'``.
callback : callable
- ``'window_resize_event'``: called with ``(width, height)``
in logical pixels; fires continuously while resizing.
- ``'window_resize_end_event'``: called with no arguments;
fires once when the user releases the mouse after resizing.
- ``'window_move_event'``: called with ``(x, y)`` in Cocoa
screen coordinates (origin at bottom-left of primary screen);
fires continuously while moving.
- ``'window_move_end_event'``: called with no arguments;
fires once when the user releases the mouse after moving.
- ``'focus_in_event'``, ``'focus_out_event'``: called with
no arguments.

Returns
-------
int
A callback id that can be passed to `mpl_disconnect`.
"""
return self._window_event_callbacks.connect(event_name, callback)

def mpl_disconnect(self, cid):
"""Remove a callback previously registered with `mpl_connect`."""
self._window_event_callbacks.disconnect(cid)

def _window_resize_event(self, width, height):
self._window_event_callbacks.process('window_resize_event', width, height)

def _window_resize_end_event(self):
self._window_event_callbacks.process('window_resize_end_event')

def _window_move_event(self, x, y):
self._window_event_callbacks.process('window_move_event', x, y)

def _window_move_end_event(self):
self._window_event_callbacks.process('window_move_end_event')

def _focus_in_event(self):
self._window_event_callbacks.process('focus_in_event')

def _focus_out_event(self):
self._window_event_callbacks.process('focus_out_event')

def destroy(self):
# We need to clear any pending timers that never fired, otherwise
# we get a memory leak from the timer callbacks holding a reference
Expand Down
198 changes: 196 additions & 2 deletions src/_macosx.m
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,25 @@ @interface Window : NSWindow
- (Window*)initWithContentRect:(NSRect)rect styleMask:(unsigned int)mask backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation withManager: (PyObject*)theManager;
- (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen;
- (BOOL)closeButtonPressed;
- (PyObject*)pyManager;
@end

@interface View : NSView <NSWindowDelegate>
{ PyObject* canvas;
NSRect rubberband;
@public double device_scale;
BOOL _in_move;
id _move_monitor;
}
- (void)dealloc;
- (void)drawRect:(NSRect)rect;
- (void)updateDevicePixelRatio:(double)scale;
- (void)windowDidChangeBackingProperties:(NSNotification*)notification;
- (void)windowDidResize:(NSNotification*)notification;
- (void)windowDidMove:(NSNotification*)notification;
- (void)windowDidEndLiveResize:(NSNotification*)notification;
- (void)windowDidBecomeKey:(NSNotification*)notification;
- (void)windowDidResignKey:(NSNotification*)notification;
- (View*)initWithFrame:(NSRect)rect;
- (void)setCanvas: (PyObject*)newCanvas;
- (void)windowWillClose:(NSNotification*)notification;
Expand Down Expand Up @@ -703,7 +710,10 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name)
{
BEGIN_OBJC_ENTRY
PyObject* canvas;
if (!PyArg_ParseTuple(args, "O", &canvas)) {
double x = 100., y = 350.;
static char *kwlist[] = {"canvas", "x", "y", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|dd", kwlist,
&canvas, &x, &y)) {
return -1;
}

Expand All @@ -721,7 +731,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name)
}
Py_DECREF(size);

NSRect rect = NSMakeRect( /* x */ 100, /* y */ 350, width, height);
NSRect rect = NSMakeRect(x, y, width, height);

self->window = [self->window initWithContentRect: rect
styleMask: NSWindowStyleMaskTitled
Expand Down Expand Up @@ -924,6 +934,74 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name)
RETURN_NULL_OR_NONE
}

static PyObject*
FigureManager_get_window_frame(FigureManager* self)
{
NSRect frame = [self->window frame];
return Py_BuildValue("dddd",
frame.origin.x, frame.origin.y,
frame.size.width, frame.size.height);
}

static PyObject*
FigureManager_set_window_frame(FigureManager* self, PyObject* args)
{
double x, y, w, h;
if (!PyArg_ParseTuple(args, "dddd", &x, &y, &w, &h)) {
return NULL;
}
[self->window setFrame: NSMakeRect(x, y, w, h) display: YES];
Py_RETURN_NONE;
}

static PyObject*
FigureManager_get_screen_frame(FigureManager* self)
{
NSScreen* screen = [self->window screen];
if (!screen) {
PyErr_SetString(PyExc_RuntimeError,
"Window is not associated with any screen");
return NULL;
}
NSRect frame = [screen frame];
return Py_BuildValue("dddd",
frame.origin.x, frame.origin.y,
frame.size.width, frame.size.height);
}

static PyObject*
FigureManager_get_window_screen_id(FigureManager* self)
{
NSScreen* screen = [self->window screen];
if (!screen) {
PyErr_SetString(PyExc_RuntimeError,
"Window is not associated with any screen");
return NULL;
}
CGDirectDisplayID displayID =
[[[screen deviceDescription] objectForKey: @"NSScreenNumber"]
unsignedIntValue];
return PyLong_FromUnsignedLong((unsigned long)displayID);
}

static PyObject*
FigureManager_set_window_level(FigureManager* self, PyObject* args)
{
int floating;
if (!PyArg_ParseTuple(args, "p", &floating)) {
return NULL;
}
[self->window setLevel: floating ? NSFloatingWindowLevel
: NSNormalWindowLevel];
Py_RETURN_NONE;
}

static PyObject*
FigureManager_get_window_level(FigureManager* self)
{
return PyBool_FromLong([self->window level] == NSFloatingWindowLevel);
}

static PyTypeObject FigureManagerType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "matplotlib.backends._macosx.FigureManager",
Expand Down Expand Up @@ -966,6 +1044,24 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name)
{"full_screen_toggle",
(PyCFunction)FigureManager_full_screen_toggle,
METH_NOARGS},
{"get_window_frame",
(PyCFunction)FigureManager_get_window_frame,
METH_NOARGS},
{"set_window_frame",
(PyCFunction)FigureManager_set_window_frame,
METH_VARARGS},
{"get_screen_frame",
(PyCFunction)FigureManager_get_screen_frame,
METH_NOARGS},
{"get_window_screen_id",
(PyCFunction)FigureManager_get_window_screen_id,
METH_NOARGS},
{"set_window_level",
(PyCFunction)FigureManager_set_window_level,
METH_VARARGS},
{"get_window_level",
(PyCFunction)FigureManager_get_window_level,
METH_NOARGS},
{} // sentinel
},
};
Expand Down Expand Up @@ -1311,6 +1407,11 @@ - (BOOL)closeButtonPressed
return YES;
}

- (PyObject*)pyManager
{
return manager;
}

- (void)close
{
[super close];
Expand All @@ -1331,11 +1432,18 @@ - (View*)initWithFrame:(NSRect)rect
self = [super initWithFrame: rect];
rubberband = NSZeroRect;
device_scale = 1;
_in_move = NO;
_move_monitor = nil;
return self;
}

- (void)dealloc
{
if (_move_monitor) {
[NSEvent removeMonitor: _move_monitor];
[_move_monitor release];
_move_monitor = nil;
}
FigureCanvas* fc = (FigureCanvas*)canvas;
if (fc) { fc->view = NULL; }
[super dealloc];
Expand Down Expand Up @@ -1509,10 +1617,64 @@ - (void)windowDidResize: (NSNotification*)notification
Py_DECREF(result);
else
PyErr_Print();
result = PyObject_CallMethod(
[window pyManager], "_window_resize_event", "ii", width, height);
if (result)
Py_DECREF(result);
else
PyErr_Print();
PyGILState_Release(gstate);
[self setNeedsDisplay: YES];
}

- (void)windowDidMove:(NSNotification*)notification
{
Window* window = (Window*)[self window];
NSRect frame = [window frame];
PyGILState_STATE gstate = PyGILState_Ensure();
PyObject* result = PyObject_CallMethod(
[window pyManager], "_window_move_event", "dd",
frame.origin.x, frame.origin.y);
if (result)
Py_DECREF(result);
else
PyErr_Print();
PyGILState_Release(gstate);

if (!_in_move) {
_in_move = YES;
__block id monitor;
__block View* blockSelf = self;
monitor = [[NSEvent
addLocalMonitorForEventsMatchingMask: NSEventMaskLeftMouseUp
handler: ^NSEvent*(NSEvent* event) {
blockSelf->_in_move = NO;
blockSelf->_move_monitor = nil;
[NSEvent removeMonitor: monitor];
[monitor release];
gil_call_method([(Window*)[blockSelf window] pyManager],
"_window_move_end_event");
return event;
}] retain];
_move_monitor = monitor;
}
}

- (void)windowDidEndLiveResize:(NSNotification*)notification
{
gil_call_method([(Window*)[self window] pyManager], "_window_resize_end_event");
}

- (void)windowDidBecomeKey:(NSNotification*)notification
{
gil_call_method([(Window*)[self window] pyManager], "_focus_in_event");
}

- (void)windowDidResignKey:(NSNotification*)notification
{
gil_call_method([(Window*)[self window] pyManager], "_focus_out_event");
}

- (void)windowWillClose:(NSNotification*)notification
{
process_event(
Expand Down Expand Up @@ -2035,6 +2197,31 @@ - (void)flagsChanged:(NSEvent *)event
},
};

static PyObject*
get_screens(PyObject* unused, PyObject* noargs)
{
NSArray<NSScreen*>* screens = [NSScreen screens];
PyObject* list = PyList_New([screens count]);
if (!list) { return NULL; }
for (NSUInteger i = 0; i < [screens count]; i++) {
NSScreen* screen = screens[i];
CGDirectDisplayID displayID =
[[[screen deviceDescription] objectForKey: @"NSScreenNumber"]
unsignedIntValue];
NSRect frame = [screen frame];
PyObject* item = Py_BuildValue("(kdddd)",
(unsigned long)displayID,
frame.origin.x, frame.origin.y,
frame.size.width, frame.size.height);
if (!item) {
Py_DECREF(list);
return NULL;
}
PyList_SET_ITEM(list, i, item);
}
return list;
}

static struct PyModuleDef moduledef = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_macosx",
Expand Down Expand Up @@ -2068,6 +2255,13 @@ - (void)flagsChanged:(NSEvent *)event
(PyCFunction)choose_save_file,
METH_VARARGS,
PyDoc_STR("Query the user for a location where to save a file.")},
{"get_screens",
(PyCFunction)get_screens,
METH_NOARGS,
PyDoc_STR(
"Return a list of (display_id, x, y, width, height) for every\n"
"connected screen, in Cocoa screen coordinates (origin at\n"
"bottom-left of the primary screen).")},
{} /* Sentinel */
},
};
Expand Down
Loading