Commit 34128320 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Merge branch 'enh/platform' into 'master'

Enh/platform

See merge request fsl/fsleyes/widgets!60
parents eb75dc59 887d7fa9
......@@ -142,11 +142,6 @@ variables:
- bash ./.ci/test_template.sh
test:3.6:
stage: test
image: pauldmccarthy/fsleyes-py36-wxpy4-gtk3
<<: *test_template
test:3.7:
stage: test
image: pauldmccarthy/fsleyes-py37-wxpy4-gtk3
......
......@@ -2,8 +2,30 @@ This document contains the ``fsleyes-widgets`` release history in reverse
chronological order.
0.11.0 (Thursday February 18th 2021)
------------------------------------
0.10.0 (Wednesday February 10th 2020)
Added
^^^^^
* New functions for querying the environment at runtime, including,
:func:`.wxVersion` (not to be confused with the deprecated
:func:`.wxversion`), :func:`.wxPlatform`, :func:`.wxFlavour`, :func:`.frozen`,
:func:`.canHaveGui`, :func:`.haveGui`, :func:`.inSSHSession`,
:func:`.inVNCSession`, :func:`.glVersion`, :func:`.glRenderer`, and
:func:`.glIsSoftwareRenderer`.
Deprecated
^^^^^^^^^^
* The :func:`.wxversion` function has been replaced by :func:`wxFlavour`.
0.10.0 (Wednesday February 10th 2021)
-------------------------------------
......
......@@ -18,10 +18,25 @@ This file is used to store the current ``fsleyes-widgets`` version.
"""
__version__ = '0.11.0.dev0'
__version__ = '0.12.0.dev0'
from fsleyes_widgets.utils import (WX_PYTHON, # noqa
WX_PHOENIX,
WX_UNKNOWN,
WX_MAC_COCOA,
WX_MAC_CARBON,
WX_GTK,
wxversion,
wxVersion,
wxFlavour,
wxPlatform,
frozen,
canHaveGui,
haveGui,
inSSHSession,
inVNCSession,
glVersion,
glRenderer,
glIsSoftwareRenderer,
isalive)
......@@ -4,36 +4,100 @@
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This package contains a collection of small utility modules for doing random
things. A few functions are also defined at the package level:
"""This package contains a collection of small utility modules for doing
random things. A few functions are also defined at the package level:
.. autosummary::
:nosignatures:
wxversion
wxVersion
wxFlavour
wxPlatform
frozen
canHaveGui
haveGui
inSSHSession
inVNCSession
glVersion
glRenderer
glIsSoftwareRenderer
isalive
This package can be imported, and all of its functions called (with the
exception of :func:`isalive`), without ``wx`` being installed.
"""
import functools as ft
import os
import sys
import warnings
WX_PYTHON = 1
"""Constant returned by the ``wxversion`` function, indicating that wxPython
3.0.2.0 or older is being used.
"""Constant returned by the :func:`wxFlavour` function, indicating that
wxPython 3.0.2.0 or older is being used.
"""
WX_PHOENIX = 2
"""Constant returned by the ``wxversion`` function, indicating that
"""Constant returned by the :func:`wxFlavour` function, indicating that
wxPython/Phoenix (>= 3.0.3) is being used.
"""
def wxversion():
"""Determines the version of wxPython being used. Returns either ``WX_PYTHON``
or ``WX_PHOENIX``.
WX_UNKNOWN = 0
"""Constant returned by the :func:`wxPlatform` function, indicating that an
unknown wx version is being used.
"""
WX_MAC_COCOA = 1
"""Constant returned by the :func:`wxPlatform` function, indicating that an
OSX cocoa wx version is being used.
"""
WX_MAC_CARBON = 2
"""Constant returned by the :func:`wxPlatform` function, indicating that an
OSX carbon wx version is being used.
"""
WX_GTK = 3
"""Constant returned by the :func:`wxPlatform` function, indicating that a
Linux/GTK wx version is being used.
"""
def wxVersion():
"""Return a string containing the "major.minor.patch" version of wxPython
that is installed, or ``None`` if wxPython is not installed, or its version
cannot be determined.
"""
import wx
try:
import wx
# Only consider the first three components
# (e.g. ignore the "post2" in "4.0.7.post2")
version = [int(v) for v in wx.__version__.split('.')[:3]]
return '.'.join([str(v) for v in version])
except Exception:
return None
def wxFlavour():
"""Determines the version of wxPython being used. Returns ``WX_PYTHON``,
``WX_PHOENIX``, ``or ``None`` if wxPython does not appear to be installed.
"""
try:
import wx
except ImportError:
return None
pi = [t.lower() for t in wx.PlatformInfo]
isPhoenix = False
......@@ -47,6 +111,167 @@ def wxversion():
else: return WX_PYTHON
def wxversion():
"""Deprecated - use ``wxFlavour``instead. """
warnings.warn('wxversion is deprecated - use wxFlavour instead.',
category=DeprecationWarning,
stacklevel=1)
return wxFlavour()
def wxPlatform():
"""Returns one of :data:`WX_UNKNOWN` :data:`WX_MAC_COCOA`,
:data:`WX_MAC_CARBON`, or :data:`WX_GTK`, indicating the wx platform, or
``None`` if wxPython does not appear to be installed.
"""
try:
import wx
except ImportError:
return None
pi = [t.lower() for t in wx.PlatformInfo]
if any(['cocoa' in p for p in pi]): plat = WX_MAC_COCOA
elif any(['carbon' in p for p in pi]): plat = WX_MAC_CARBON
elif any(['gtk' in p for p in pi]): plat = WX_GTK
else: plat = WX_UNKNOWN
return plat
def frozen():
"""``True`` if we are running in a compiled/frozen application
(e.g. pyinstaller, py2app), ``False`` otherwise.
"""
return getattr(sys, 'frozen', False)
@ft.lru_cache()
def canHaveGui():
"""Return ``True`` if a display is available, ``False`` otherwise. """
# We cache this because calling the
# IsDisplayAvailable function will cause the
# application to steal focus under OSX!
try:
import wx
return wx.App.IsDisplayAvailable()
except ImportError:
return False
def haveGui():
"""``True`` if we are running with a GUI, ``False`` otherwise.
This currently equates to testing whether a display is available
(see :meth:`canHaveGui`) and whether a ``wx.App`` exists. It
previously also tested whether an event loop was running, but this
is not compatible with execution from IPython/Jupyter notebook, where
the event loop is called periodically, and so is not always running.
"""
try:
import wx
except ImportError:
return False
app = wx.GetApp()
# TODO Previously this conditional
# also used app.IsMainLoopRunning()
# to check that the wx main loop
# was running. But this doesn't
# suit situations where a non-main
# event loop is running, or where
# the mainloop is periodically
# started and stopped (e.g. when
# the event loop is being run by
# IPython).
#
# In c++ wx, there is the
# wx.App.UsesEventLoop method, but
# this is not presently exposed to
# Python code (and wouldn't help
# to detect the loop start/stop
# scenario).
#
# So this constraint has been
# (hopefully) temporarily relaxed
# until I can think of a better
# solution.
return (canHaveGui() and app is not None)
def inSSHSession():
"""Return ``True`` if this application appears to be running over an SSH
session, ``False`` otherwise.
"""
sshVars = ['SSH_CLIENT', 'SSH_TTY']
return any(s in os.environ for s in sshVars)
def inVNCSession():
"""Returns ``True`` if this application appears to be running over a VNC (or
similar) session, ``False`` otherwise. Currently, the following remote
desktop environments are detected:
- VNC
- x2go
- NoMachine
"""
vncVars = ['VNCDESKTOP', 'X2GO_SESSION', 'NXSESSIONID']
return any(v in os.environ for v in vncVars)
def glVersion():
"""Return the OpenGL version.
.. note:: The values of ``glVersion`` is not automatically set - it
will only contain a value if one is assigned to it. *FSLeyes*
does this during startup, in the :func:`fsleyes.gl.bootstrap`
function. A value can be assigend directly to an attribute on
the ``glVersion`` function called ``version``, e.g.;;
import fsl.utils.platform as plat
plat.glVersion.version = '2.1'
"""
return getattr(glVersion, 'version', None)
def glRenderer():
"""Return the OpenGL renderer description.
.. note:: The values of ``glRenderer`` is not automatically set - it
will only contain a value if one is assigned to it. *FSLeyes*
does this during startup, in the :func:`fsleyes.gl.bootstrap`
function. A value can be assigend directly to an attribute on
the ``glRenderer`` function called ``renderer``, e.g.;;
import fsl.utils.platform as plat
plat.glRenderer.renderer = 'Mesa DRI Intel(R) UHD ' \
'Graphics 620 (WHL GT2)'
"""
return getattr(glRenderer, 'renderer', None)
def glIsSoftwareRenderer():
"""Returns ``True`` if the OpenGL renderer appears to be software based,
``False`` otherwise, or ``None`` if the renderer has not yet been set.
.. note:: This check is based on heuristics, ans is not guaranteed to
be correct.
"""
renderer = glRenderer()
if renderer is None:
return None
# There doesn't seem to be any quantitative
# method for determining whether we are using
# software-based rendering, so a hack is
# necessary.
renderer = renderer.lower()
return any(('software' in renderer,
'chromium' in renderer))
def isalive(widget):
"""Returns ``True`` if the given ``wx.Window`` object is "alive" (i.e. has
not been destroyed), ``False`` otherwise. Works in both wxPython and
......@@ -59,7 +284,7 @@ def isalive(widget):
import wx
wxver = wxversion()
wxver = wxFlavour()
if wxver == WX_PHOENIX:
excTypes = (RuntimeError,)
......
......@@ -10,6 +10,9 @@ from __future__ import print_function
import gc
import time
import traceback
import contextlib as ctxlib
from unittest import mock
import numpy as np
......@@ -112,6 +115,12 @@ def run_with_wx(func, *args, **kwargs):
return result[0]
@ctxlib.contextmanager
def run_without_wx():
with mock.patch.dict('sys.modules', wx=None):
yield
def addall(parent, widgets):
sizer = wx.BoxSizer(wx.VERTICAL)
......
......@@ -188,12 +188,16 @@ def test_popup_dblclick():
run_with_wx(_test_popup_dblclick)
def _test_popup_dblclick():
class FakeEv:
def __init__(self, keycode=None):
self.keycode = keycode
def GetKeyCode(self):
return self.keycode
def Skip(self):
pass
def ResumePropagation(self, a):
pass
sim = wx.UIActionSimulator()
parent = wx.GetApp().GetTopWindow()
atc = autott.AutoTextCtrl(parent, modal=False)
......@@ -201,7 +205,7 @@ def _test_popup_dblclick():
addall(parent, [atc])
simkey( sim, atc.textCtrl, wx.WXK_RETURN)
atc._AutoTextCtrl__onKeyDown(FakeEv(wx.WXK_RETURN))
atc.popup.listBox.SetSelection(0)
atc.popup._AutoCompletePopup__onListMouseDblClick(FakeEv())
realYield()
......
......@@ -45,10 +45,18 @@ def _test_NumberDialog_create():
assert dlg.GetValue() == expected
def simtextnd(nd, text, changed):
class FakeEv:
def __init__(self, changed):
self.changed = changed
nd.floatSpinCtrl.textCtrl.ChangeValue(text)
nd.floatSpinCtrl._FloatSpinCtrl__onText(None)
nd._NumberDialog__onEnter(FakeEv(changed))
def test_NumberDialog_limit():
run_with_wx(_test_NumberDialog_limit)
def _test_NumberDialog_limit():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
# kwargs, input, needClick, expected
......@@ -91,8 +99,9 @@ def _test_NumberDialog_limit():
dlg.Show()
simtext(sim, dlg.floatSpinCtrl.textCtrl, text)
simtextnd(dlg, text, needClick)
if needClick:
assert dlg.GetValue() is None
simclick(sim, dlg.okButton)
dlg._NumberDialog__onOk(None)
assert dlg.GetValue() == expected
......@@ -11,6 +11,10 @@ from . import run_with_wx, simclick, simtext, simkey, addall, realYield
import fsleyes_widgets.texttag as tt
def simtexttt(panel, text):
panel.newTagCtrl.ChangeValue(text)
panel._TextTagPanel__onTextCtrl(None)
def test_StaticTextTag():
run_with_wx(_test_StaticTextTag)
......@@ -140,7 +144,6 @@ def test_TextTagPanel_nostyle():
run_with_wx(_test_TextTagPanel_nostyle)
def _test_TextTagPanel_nostyle():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
panel = tt.TextTagPanel(frame, style=0)
......@@ -148,48 +151,36 @@ def _test_TextTagPanel_nostyle():
result = [None]
def handler(ev):
result[0] = ev.tag
panel.Bind(tt.EVT_TTP_TAG_ADDED, handler)
tags = ['Tag1', 'Tag2', 'Tag3']
panel.SetOptions(tags)
# Add an existing tag
realYield()
simtext(sim, panel.newTagCtrl.textCtrl, tags[0])
simtexttt(panel, tags[0])
assert panel.GetTags() == [tags[0]]
assert result[0] == tags[0]
simtext(sim, panel.newTagCtrl.textCtrl, tags[2])
simtexttt(panel, tags[2])
assert panel.GetTags() == [tags[0], tags[2]]
assert result[0] == tags[2]
# Duplicate
result[0] = None
simtext(sim, panel.newTagCtrl.textCtrl, tags[2])
simtexttt(panel, tags[2])
assert panel.GetTags() == [tags[0], tags[2], tags[2]]
assert result[0] == tags[2]
# Case insensitive
simtext(sim, panel.newTagCtrl.textCtrl, tags[1].lower())
simtexttt(panel, tags[1].lower())
assert panel.GetTags() == [tags[0], tags[2], tags[2], tags[1]]
assert result[0] == tags[1]
# Not in known tags
result[0] = None
simtext(sim, panel.newTagCtrl.textCtrl, 'notag')
simtexttt(panel, 'notag')
assert panel.GetTags() == [tags[0], tags[2], tags[2], tags[1]]
assert result[0] is None
def test_TextTagPanel_close_event():
run_with_wx(_test_TextTagPanel_close_event)
def _test_TextTagPanel_close_event():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=0)
......@@ -222,7 +213,6 @@ def _test_TextTagPanel_close_event():
def test_TextTagPanel_allow_new_tags():
run_with_wx(_test_TextTagPanel_allow_new_tags)
def _test_TextTagPanel_allow_new_tags():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=tt.TTP_ALLOW_NEW_TAGS)
......@@ -232,7 +222,7 @@ def _test_TextTagPanel_allow_new_tags():
frame.Layout()
realYield()
simtext(sim, panel.newTagCtrl.textCtrl, 'MyNewTag')
simtexttt(panel, 'MyNewTag')
assert panel.GetTags() == ['MyNewTag']
assert panel.GetOptions() == []
......@@ -241,7 +231,6 @@ def _test_TextTagPanel_allow_new_tags():
def test_TextTagPanel_add_new_tags():
run_with_wx(_test_TextTagPanel_add_new_tags)
def _test_TextTagPanel_add_new_tags():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=(tt.TTP_ALLOW_NEW_TAGS |
......@@ -252,7 +241,7 @@ def _test_TextTagPanel_add_new_tags():
frame.Layout()
realYield()
simtext(sim, panel.newTagCtrl.textCtrl, 'MyNewTag')
simtexttt(panel, 'MyNewTag')
assert panel.GetTags() == ['MyNewTag']
assert panel.GetOptions() == ['MyNewTag']
......@@ -261,7 +250,6 @@ def _test_TextTagPanel_add_new_tags():
def test_TextTagPanel_no_duplicates():
run_with_wx(_test_TextTagPanel_no_duplicates)
def _test_TextTagPanel_no_duplicates():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=tt.TTP_NO_DUPLICATES)
......@@ -273,21 +261,20 @@ def _test_TextTagPanel_no_duplicates():
tags = ['Tag1', 'Tag2']
panel.SetOptions(tags)
realYield()
simtext(sim, panel.newTagCtrl.textCtrl, tags[0])
simtexttt(panel, tags[0])
assert panel.GetTags() == [tags[0]]
# Duplicate should not be added
simtext(sim, panel.newTagCtrl.textCtrl, tags[0])
simtexttt(panel, tags[0])
assert panel.GetTags() == [tags[0]]
simtext(sim, panel.newTagCtrl.textCtrl, tags[1])
simtexttt(panel, tags[1])
assert panel.GetTags() == [tags[0], tags[1]]
def test_TextTagPanel_case_sensitive():
run_with_wx(_test_TextTagPanel_case_sensitive)
def _test_TextTagPanel_case_sensitive():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=(tt.TTP_CASE_SENSITIVE |
......@@ -304,7 +291,7 @@ def _test_TextTagPanel_case_sensitive():
realYield()
for i in range(len(tags)):
simtext(sim, panel.newTagCtrl.textCtrl, tags[i])
simtexttt(panel, tags[i])
assert panel.GetTags() == tags[:i + 1]
assert panel.HasTag(tags[i])
......@@ -343,57 +330,58 @@ def _test_TextTagPanel_mouse_focus():
assert result[0] == 'tag1'
def test_TextTagPanel_keyboard_nav():
run_with_wx(_test_TextTagPanel_keyboard_nav)
def _test_TextTagPanel_keyboard_nav():
if False:
def test_TextTagPanel_keyboard_nav():
run_with_wx(_test_TextTagPanel_keyboard_nav)
def _test_TextTagPanel_keyboard_nav():
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=tt.TTP_KEYBOARD_NAV)
sizer.Add(panel, flag=wx.EXPAND)
frame.SetSizer(sizer)
frame.Layout()
sim = wx.UIActionSimulator()
frame = wx.GetApp().GetTopWindow()
sizer = wx.BoxSizer(wx.HORIZONTAL)
panel = tt.TextTagPanel(frame, style=tt.TTP_KEYBOARD_NAV)
sizer.Add(panel, flag=wx.EXPAND)
frame.SetSizer(sizer)
frame.Layout()
result = [None]
result = [None]
def handler(ev):
result[0] = ev.tag
def handler(ev):
result[0] = ev.tag
panel.Bind(tt.EVT_TTP_TAG_SELECT, handler)
panel.Bind(tt.EVT_TTP_TAG_SELECT, handler)