Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:rproepp/spykeviewer into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
rproepp committed Aug 21, 2014
2 parents f4db3c4 + eab60cb commit 60bef1c
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 52 deletions.
11 changes: 9 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Version 0.4.0
* Data file path transform for starting plugins remotely.

Version 0.4.1
-------------
* IPython 1.0 supported
* IPython now supported as dock instead of external console
* More explicit error messages on file loading failures.
* Added "Start plugin remotely" to toolbar.

Version 0.4.0
-------------
* Optional transparent lazy loading and lazy cascading for supported IOs.
* Splash screen while loading the application.
* Open files dialog as an alternative to the "Files" dock.
Expand All @@ -24,7 +32,6 @@ Version 0.4.0

Version 0.3.0
-------------

* Added search and replace functionality to plugin editor (access with
``Ctrl`` + ``F`` and ``Ctrl`` + ``H``).
* Added startup script. Can be modified using File->Edit startup script.
Expand Down
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2012, Robert Pröpper and conbtributors.
Copyright (c) 2012-2014, Robert Pröpper and conbtributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand All @@ -21,4 +21,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 changes: 11 additions & 1 deletion doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ objects to access the main window and application and convenience functions.
as for :class:`subprocess.Popen`, e.g.
``['--param1', 'first value', '-p2', '2']``. Default: ``[]``

remote_path_transform (function)
When the remote script is used to start plugins on a different
computer, the paths of data files can change. This function can be
used to change the path of all data files sent to a remote script.
For example, if the data files are in the same directory where the
plugin is started on the remote computer, you can strip the path and
just keep the filename:
``spykeviewer.api.config.remote_path_transform = lambda x: os.path.split(x)[1]``
Default: The identity, paths are not changed.


.. data:: spykeviewer.api.window

Expand All @@ -93,4 +103,4 @@ objects to access the main window and application and convenience functions.

.. autofunction:: spykeviewer.api.start_plugin

.. autofunction:: spykeviewer.api.get_plugin
.. autofunction:: spykeviewer.api.get_plugin
17 changes: 11 additions & 6 deletions spykeviewer/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def __init__(self):
self.autoselect_channels = True
# Select all visible units in navigation by default
self.autoselect_units = False
# Tranformation function for file paths when starting plugin remotely
self.remote_path_transform = lambda x: x

def __setitem__(self, key, value):
self.__dict__[key] = value
Expand All @@ -54,23 +56,26 @@ def start_plugin(name, current=None, selections=None):
selections. If ``None``, the regular selections from the GUI
are used.
"""
return window.start_plugin(name, current, selections)
return window.start_plugin(name, current, selections, False)


def start_plugin_remote(name, current=None, selections=None):
def start_plugin_remote(plugin, current=None, selections=None):
""" Start first plugin with given name using the remote script.
Raises a SpykeException if not exactly one plugins with
this name exist.
:param str name: The name of the plugin. Should not include the
directory.
:param plugin: The name of the plugin or the plugin object. If this
is a string, it should not include the directory and the
plugin with this name will be started using the remote script.
If it is a plugin object, this plugin will be started using
the remote script.
:param current: A DataProvider to use as current selection. If
``None``, the regular current selection from the GUI is used.
:param list selections: A list of DataProvider objects to use as
selections. If ``None``, the regular selections from the GUI
are used.
"""
window.start_plugin_remote(name, current, selections)
window.start_plugin_remote(plugin, current, selections)


def get_plugin(name):
Expand Down Expand Up @@ -98,4 +103,4 @@ def load_files(file_paths):
:param list file_paths: The files to open as strings with the full
file path.
"""
window.load_files(file_paths)
window.load_files(file_paths)
23 changes: 8 additions & 15 deletions spykeviewer/ui/checkable_item_delegate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QStyledItemDelegate, QStyle, QApplication


class CheckableItemDelegate(QStyledItemDelegate):
""" A StyledItemDelegate for checkable items with support for both
checkboxes and radio buttons.
Expand All @@ -21,8 +22,8 @@ def paint(self, painter, option, index):

if index.data(CheckableItemDelegate.CheckTypeRole):
# Size and spacing in current style
is_radio = index.data(CheckableItemDelegate.CheckTypeRole) ==\
CheckableItemDelegate.RadioCheckType
is_radio = index.data(CheckableItemDelegate.CheckTypeRole) == \
CheckableItemDelegate.RadioCheckType
if is_radio:
button_width = style.pixelMetric(
QStyle.PM_ExclusiveIndicatorWidth, option)
Expand All @@ -34,7 +35,6 @@ def paint(self, painter, option, index):
spacing = style.pixelMetric(
QStyle.PM_CheckBoxLabelSpacing, option)


# Draw default appearance shifted to right
myOption = option
left = myOption.rect.left()
Expand All @@ -51,26 +51,19 @@ def paint(self, painter, option, index):
myOption.state |= QStyle.State_Off

if is_radio:
style.drawPrimitive(QStyle.PE_IndicatorRadioButton, myOption, painter)
style.drawPrimitive(
QStyle.PE_IndicatorRadioButton, myOption, painter)
else:
style.drawPrimitive(QStyle.PE_IndicatorCheckBox, myOption, painter)
style.drawPrimitive(
QStyle.PE_IndicatorCheckBox, myOption, painter)
else:
QStyledItemDelegate.paint(self, painter, option, index)

def sizeHint(self, option, index):
s = QStyledItemDelegate.sizeHint(self, option, index)
# sizeHint is for some reason only called once, so set
# size globally
#if index.data(CheckableItemDelegate.CheckTypeRole):
# # Determine size of check buttons in current style
# #noinspection PyArgumentList
# button_height = QApplication.style().pixelMetric(QStyle.PM_ExclusiveIndicatorHeight, option)
# # Ensure that row is tall enough to draw check button
# print button_height
# s.setHeight(max(s.height(), button_height))
radio_height = QApplication.style().pixelMetric(
QStyle.PM_ExclusiveIndicatorHeight, option)
check_height = QApplication.style().pixelMetric(
QStyle.PM_IndicatorHeight, option)
s.setHeight(max(s.height(), radio_height, check_height))
return s
return s
63 changes: 44 additions & 19 deletions spykeviewer/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,16 +544,21 @@ def run_command(self, cmd, history=True, new_prompt=True):
# Duplicate stdout and stderr for console
# Not using previous stdout, only stderr. Using StreamDuplicator
# because spyder stream does not have flush() method...
ch = logging.StreamHandler()
ch.setLevel(logging.WARNING)
logger.addHandler(ch)

sys.stdout = StreamDuplicator([sys.stdout])
sys.stderr = StreamDuplicator([sys.stderr, sys.__stderr__])

# Set root logging handler to print all log warnings in console
ch = logging.StreamHandler()
ch.setLevel(logging.WARNING)
root_logger = logging.getLogger()
root_logger.addHandler(ch)

def _append_python_history(self):
self.browser.refresh_table()
self.history.append('\n' + self.console.history[-1])
try:
self.history.append('\n' + self.console.history[-1])
except IndexError:
pass # Empty history, not a problem
self.history.set_cursor_position('eof')

def on_ipyDock_visibilityChanged(self, visible):
Expand Down Expand Up @@ -1118,7 +1123,8 @@ def _save_plugin_before_run(self):
return None
return ana

def _run_plugin(self, plugin, current=None, selections=None):
def _run_plugin(self, plugin, current=None, selections=None,
finish_progress=True):
if current is None:
current = self.provider
if selections is None:
Expand All @@ -1141,7 +1147,8 @@ def _run_plugin(self, plugin, current=None, selections=None):
traceback.print_exception(type(e), e, tb)
return None
finally:
self.progress.done()
if finish_progress:
self.progress.done()

@pyqtSignature("")
def on_actionEditPlugin_triggered(self):
Expand Down Expand Up @@ -1335,10 +1342,22 @@ def get_plugin(self, name):

return plugins[0]

def start_plugin(self, name, current=None, selections=None):
def start_plugin(self, name, current=None, selections=None,
finish_progress=True):
""" Start first plugin with given name and return result of start()
method. Raises a SpykeException if not exactly one plugins with
this name exist.
:param str name: Name of the plugin (as specified by the get_name()
method in the plugin.
:param current: A DataProvider to use as current selection. If
``None``, the regular current selection from the GUI is used.
Default: ``None``
:param list selections: A list of DataProvider objects to use as
selections. If ``None``, the regular selections from the GUI
are used. Default: ``None``
:param bool finish_progress: If ``True``, progress indicators are
closed automatically after the plugin finishes.
"""
plugins = self.plugin_model.get_plugins_for_name(name)
if not plugins:
Expand All @@ -1359,20 +1378,24 @@ def start_plugin(self, name, current=None, selections=None):
raise SpykeException(
'Multiple plugins named "%s" exist!' % name)

return self._run_plugin(plugins[0], current, selections)
return self._run_plugin(plugins[0], current, selections,
finish_progress)

def start_plugin_remote(self, name, current=None, selections=None):
""" Start first plugin with given name remotely. Does not return
any value. Raises a SpykeException if not exactly one plugins with
this name exist.
def start_plugin_remote(self, plugin, current=None, selections=None):
""" Start given plugin (or plugin with given name) remotely.
Does not return any value. Raises a SpykeException if not
exactly one plugins with this name exist.
"""
plugins = self.plugin_model.get_plugins_for_name(name)
if not plugins:
raise SpykeException('No plugin named "%s" exists!' % name)
if len(plugins) > 1:
raise SpykeException('Multiple plugins named "%s" exist!' % name)
if not isinstance(plugin, AnalysisPlugin):
plugins = self.plugin_model.get_plugins_for_name(plugin)
if not plugins:
raise SpykeException('No plugin named "%s" exists!' % plugin)
if len(plugins) > 1:
raise SpykeException(
'Multiple plugins named "%s" exist!' % plugin)
plugin = plugins[0]

self._execute_remote_plugin(plugins[0], current, selections)
self._execute_remote_plugin(plugin, current, selections)

def on_file_available(self, available):
""" Callback when availability of a file for a plugin changes.
Expand All @@ -1395,6 +1418,7 @@ def on_actionSettings_triggered(self):
self.selection_path = settings.selection_path()
self.filter_path = settings.filter_path()
self.remote_script = settings.remote_script()
AnalysisPlugin.data_dir = settings.data_path()
self.plugin_paths = settings.plugin_paths()
if self.plugin_paths:
self.pluginEditorDock.set_default_path(self.plugin_paths[-1])
Expand Down Expand Up @@ -1503,6 +1527,7 @@ def closeEvent(self, event):
settings.setValue('selectionPath', self.selection_path)
settings.setValue('filterPath', self.filter_path)
settings.setValue('remoteScript', self.remote_script)
settings.setValue('dataPath', AnalysisPlugin.data_dir)

# Save plugin configurations
configs = self.get_plugin_configs()
Expand Down
40 changes: 33 additions & 7 deletions spykeviewer/ui/main_window_neo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
from spykeutils.plugin.data_provider_stored import NeoStoredProvider
from spykeutils.plugin import io_plugin

from main_window import MainWindow
from .main_window import MainWindow
from ..plugin_framework.data_provider_viewer import NeoViewerProvider
from neo_navigation import NeoNavigationDock
from dir_files_dialog import DirFilesDialog
import io_settings
from .neo_navigation import NeoNavigationDock
from .dir_files_dialog import DirFilesDialog
from . import io_settings
from .. import api

logger = logging.getLogger('spykeviewer')

Expand Down Expand Up @@ -172,9 +173,14 @@ def __init__(self, paths):
QThread.__init__(self)
self.paths = paths
self.blocks = []
self.error = None

def run(self):
self.blocks = NeoDataProvider.get_blocks(self.paths[0])
try:
self.blocks = NeoDataProvider.get_blocks(self.paths[0])
except Exception as e:
self.error = e
raise

def load_files(self, file_paths):
self.progress.begin('Loading data files...')
Expand Down Expand Up @@ -217,13 +223,30 @@ def load_file_callback(self):
# Load worker thread finished
blocks = self.load_worker.blocks
if blocks is None:
logger.error('Could not read "%s"' %
QMessageBox.critical(
self, 'File could not be loaded',
'Could not read "%s": No suitable IO found.' %
self.load_worker.paths[0])
logger.error('Could not read "%s": No suitable IO found.' %
self.load_worker.paths[0])
self.progress.done()
self.load_progress.reset()
self.raise_()
return

if self.load_worker.error:
QMessageBox.critical(
self, 'File could not be loaded',
'While loading "%s", the following error occured:'
'\n\n%s\n%s\n\nSee the console for more details.' % (
self.load_worker.paths[0],
type(self.load_worker.error).__name__,
str(self.load_worker.error)))
self.progress.done()
self.load_progress.reset()
self.raise_()
return

for block in blocks:
name = block.name
if not name or name == 'One segment only':
Expand Down Expand Up @@ -539,6 +562,8 @@ def _execute_remote_plugin(self, plugin, current=None, selections=None):
sl.append(s.data_dict())

io_plugin_files = []
transform_path = getattr(api.config, 'remote_path_transform',
lambda x: x)
for s in sl:
if s['type'] != 'Neo':
continue
Expand All @@ -550,6 +575,7 @@ def _execute_remote_plugin(self, plugin, current=None, selections=None):
'%s is not a known IO class!' % b[2])
if getattr(io, '_is_spyke_plugin', False):
io_plugin_files.append(io._python_file)
b[1] = transform_path(b[1])

selections = json.dumps(sl, sort_keys=True, indent=2)
config = pickle.dumps(plugin.get_parameters())
Expand All @@ -560,4 +586,4 @@ def _execute_remote_plugin(self, plugin, current=None, selections=None):
def closeEvent(self, event):
super(MainWindowNeo, self).closeEvent(event)

NeoDataProvider.clear()
NeoDataProvider.clear()

0 comments on commit 60bef1c

Please sign in to comment.