[app] move code to nixview package

This commit is contained in:
2021-01-09 12:11:16 +01:00
parent 5cf768d72c
commit 1b14dd5695
8 changed files with 0 additions and 0 deletions

259
nixview/central_widget.py Normal file
View File

@@ -0,0 +1,259 @@
import os
from PyQt5.QtWidgets import QComboBox, QFrame, QGroupBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QSplitter, QStackedLayout, QAbstractItemView, QTextEdit, QVBoxLayout, QWidget, QGridLayout
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import QItemSelectionModel, Qt, QSettings, pyqtSignal
from file_handler import FileHandler, ItemDescriptor
from plot_screen import PlotScreen
import communicator as comm
import constants as cnst
from tree_model import NixTreeView, TreeModel, TreeType
class CentralWidget(QWidget):
def __init__(self, parent=None) -> None:
super().__init__(parent=parent)
self._splash = SplashScreen()
self._file_view = FileView(self)
self._plot_screen = PlotScreen(self)
self._plot_screen.close_signal.connect(self.on_plot_close)
self._stack = QStackedLayout(self)
self._stack.addWidget(self._splash)
self._stack.addWidget(self._file_view)
self._stack.addWidget(self._plot_screen)
self.setLayout(self._stack)
def show_file_content(self):
self._stack.setCurrentIndex(1)
self._file_view.update()
def plot_item(self, item):
self._stack.setCurrentIndex(2)
self._plot_screen.plot(item)
def on_plot_close(self):
self._stack.setCurrentIndex(1)
def reset(self):
self._file_view.reset()
self._splash.reset()
self._stack.setCurrentIndex(0)
class FileView(QWidget):
def __init__(self, parent=None) -> None:
super().__init__(parent=parent)
self._file_handler = FileHandler()
vbox = QVBoxLayout()
self.setLayout(vbox)
main_splitter = QSplitter(Qt.Vertical)
self.layout().addWidget(main_splitter)
self._info = EntityInfo(self)
main_splitter.addWidget(self._info)
self._data_tree = NixTreeView(self)
self._tree_type_combo = QComboBox()
self._tree_type_combo.adjustSize()
self._tree_type_combo.addItems([TreeType.Data.value, TreeType.Full.value, TreeType.Metadata.value])
self._tree_type_combo.currentTextChanged.connect(self.update)
hbox = QHBoxLayout()
hbox.addWidget(QLabel("Tree type:"))
hbox.addWidget(self._tree_type_combo)
hbox.addStretch()
data_group = QGroupBox("Data")
data_vbox = QVBoxLayout()
data_vbox.setContentsMargins(1, 10, 1, 1)
data_vbox.addLayout(hbox)
data_vbox.addWidget(self._data_tree)
data_group.setLayout(data_vbox)
main_splitter.addWidget(data_group)
main_splitter.setSizes([200, 600])
vbox.addWidget(main_splitter)
def dataTreeSelection(self, current_index, last_index):
if not current_index.isValid():
return
item = current_index.internalPointer()
comm.communicator.item_selected.emit(item)
self._info.setEntityInfo(item.node_descriptor)
def update(self):
tt = TreeType.Data
if self._tree_type_combo.currentText() == TreeType.Data.value:
tt = TreeType.Data
elif self._tree_type_combo.currentText() == TreeType.Full.value:
tt = TreeType.Full
elif self._tree_type_combo.currentText() == TreeType.Metadata.value:
tt = TreeType.Metadata
self._info.setEntityInfo(None)
data_model = TreeModel(self._file_handler, tt)
self._data_tree.setModel(data_model)
selection_model = QItemSelectionModel(data_model)
self._data_tree.setSelectionModel(selection_model)
selection_model.currentChanged.connect(self.dataTreeSelection)
for i in range(data_model.columnCount(None)):
self._data_tree.resizeColumnToContents(i)
self._info.setFileInfo(self._file_handler.file_descriptor)
def reset(self):
pass
class EntityInfo(QWidget):
def __init__(self, parent):
super().__init__(parent=parent)
self._file_handler = FileHandler()
self.setLayout(QHBoxLayout())
self._metadata_tree = NixTreeView()
mdata_grp = QGroupBox("Metadata")
mdata_grp.setLayout(QVBoxLayout())
mdata_grp.layout().setContentsMargins(1, 10, 1, 1)
mdata_grp.layout().addWidget(self._metadata_tree)
file_info_grp = QGroupBox("File info")
file_info_grp.setLayout(QVBoxLayout())
file_info_grp.layout().setContentsMargins(1, 10, 1, 1)
self._file_info = QTextEdit("File information")
self._file_info.setEnabled(True)
self._file_info.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse)
self._file_info.setFrameShape(QFrame.NoFrame)
self._file_info.setLineWrapMode(QTextEdit.WidgetWidth)
file_info_grp.layout().addWidget(self._file_info)
entity_info_grp = QGroupBox("Entity info")
entity_info_grp.setLayout(QVBoxLayout())
entity_info_grp.layout().setContentsMargins(1, 10, 1, 1)
self._entity_info = QTextEdit("Entity information")
self._file_info.setEnabled(True)
self._file_info.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse)
self._file_info.setFrameShape(QFrame.NoFrame)
self._file_info.setLineWrapMode(QTextEdit.WidgetWidth)
entity_info_grp.layout().addWidget(self._entity_info)
self._splitter = QSplitter(Qt.Horizontal)
self._splitter.addWidget(file_info_grp)
self._splitter.addWidget(entity_info_grp)
self._splitter.addWidget(mdata_grp)
self._splitter.setSizes([200, 400, 0])
self._splitter.setStretchFactor(0, 0)
self._splitter.setStretchFactor(1, 1)
self._splitter.setStretchFactor(2, 1)
self.layout().addWidget(self._splitter)
def setFileInfo(self, file_info):
if file_info is not None:
self._file_info.setText(file_info.toHtml())
def setEntityInfo(self, entity_info):
if entity_info is None or not isinstance(entity_info, ItemDescriptor):
self._splitter.setSizes([200, 400, 0])
self._entity_info.setText("")
self._metadata_tree.setModel(None)
return
if entity_info.metadata_id is not None:
self._splitter.setSizes([200, 400, 400])
else:
self._splitter.setSizes([200, 400, 0])
self._entity_info.setText(entity_info.to_html())
metadata_model = TreeModel(self._file_handler, TreeType.Metadata, root_section_id=entity_info.metadata_id)
self._metadata_tree.setModel(metadata_model)
for i in range(metadata_model.columnCount(None)):
self._metadata_tree.resizeColumnToContents(i)
class SplashScreen(QWidget):
keyPressed = pyqtSignal(int)
def __init__(self, parent=None) -> None:
super().__init__(parent=parent)
#self.setStyleSheet("background-color: white;")
layout = QGridLayout()
layout.setColumnStretch(0, 2)
layout.setColumnStretch(1, 1)
layout.setColumnStretch(2, 1)
layout.setColumnStretch(3, 1)
layout.setColumnStretch(4, 2)
layout.setRowStretch(0, 1)
layout.setRowStretch(1, 0)
layout.setRowStretch(2, 1)
layout.setRowStretch(3, 2)
self.setLayout(layout)
label = QLabel()
label.setPixmap(QPixmap("./icons/nixview_transparent.png"))
label.setMaximumWidth(300)
label.setAlignment(Qt.AlignCenter)
layout.addWidget(label, 1, 1, 1, 3, Qt.AlignCenter)
frame = QFrame()
l = QVBoxLayout()
l.addWidget(QLabel("Recently opened files:"))
self._file_list = QListWidget(self)
self._file_list.setSelectionMode(QAbstractItemView.SingleSelection)
self._file_list.itemClicked.connect(self._on_file_clicked)
self._file_list.setFrameShape(QFrame.NoFrame)
self.keyPressed.connect(self._on_key_pressed)
l.addWidget(self._file_list)
frame.setLayout(l)
layout.addWidget(frame, 3, 1, 1, 3)
self._file_map = {}
self._read_recent_files()
def keyPressEvent(self, event):
super(SplashScreen, self).keyPressEvent(event)
if event.key() == Qt.Key_Return:
self.keyPressed.emit(event.key())
def _create_short_filename(self, original, index, max_len=40):
short = original
parts = original.split(os.sep)
if len(parts) == 1:
short = "%i: %s" % (index, short[:max_len])
else:
post = parts[-1]
if len(post) > max_len - 4:
post = post[:max_len - 4]
short = str("%i: " % index) + "... " + post
else:
short = str("%i: " % index) + " ... ".join([original[:max_len - len(post) - 4], post])
return short
def _read_recent_files(self):
settings = QSettings(cnst.organization, cnst.application)
filenames = settings.value(cnst.settings_recent_files_key, [])
del settings
for i, f in enumerate(filenames):
shortname = self._create_short_filename(f, i + 1, max_len=38)
self._file_map[shortname] = f
item = QListWidgetItem(shortname)
item.setToolTip(f)
self._file_list.addItem(item)
def reset(self):
self._file_list.clear()
self._read_recent_files()
def _on_file_clicked(self, item):
comm.communicator.open_recent.emit(self._file_map[item.text()])
def _on_key_pressed(self, key):
item = self._file_list.currentItem()
if item is not None and key == Qt.Key_Return:
comm.communicator.open_recent.emit(self._file_map[item.text()])

12
nixview/communicator.py Normal file
View File

@@ -0,0 +1,12 @@
from tree_model import NixTreeItem
from PyQt5.QtCore import QObject, pyqtSignal
class Communicate(QObject):
"""
Small class for across app communication. Defines signals, to which other classes can connect.
"""
open_recent = pyqtSignal(str)
item_selected = pyqtSignal(NixTreeItem)
communicator = Communicate()

7
nixview/constants.py Normal file
View File

@@ -0,0 +1,7 @@
organization = "nixio"
application = "nixview"
version = 0.1
settings_recent_files_key = "/".join([organization, application, "recent_files"])
settings_recent_file_max_count_key = "/".join([organization, application, "recent_files_max_count"])
settings_recent_file_max_count = 10

357
nixview/file_handler.py Normal file
View File

@@ -0,0 +1,357 @@
import os
import nixio as nix
from enum import Enum
import datetime as dt
class ItemDescriptor():
def __init__(self, name=None, id=None, type=None, value=None, definition=None, block_id=None, entity_type=None, shape=None, metadata=None, data_type=None, source_id=None, created_at=None, updated_at=None) -> None:
super().__init__()
self.name = name
self.type = type
self.id = id
self.block_id= block_id
self.definition = definition
self.value = value
self.entity_type = entity_type
self.data_type = data_type
self.shape = shape
self.metadata_id = metadata
self.source_id = source_id
self.created_at = None
self.updated_at = None
def to_html(self):
descr = "<html><h4>%s: %s</h4>" % (self.type, self.name)
descr += "<ol style='list-style-type:none'>"
descr += "<li><small><b>id:</b> %s</small></li>" % (self.id)
descr += "<li><small><b>entity type:</b> %s</small></li>" % (self.entity_type.value)
descr += "<li><small><b>data type:</b> %s</small></li>" % (str(self.data_type))
descr += "<li><small><b>data shape:</b> %s</small></li>" % (str(self.shape))
descr += "<hr>"
descr += "<p><small><b>definition:</b> %s</small></p>" % (self.definition)
descr += "<hr>"
descr += "<li><small><b>metadata id:</b> %s</small></li>" % (self.metadata_id)
descr += "<li><small><b>source id:</b> %s</small></li>" % (self.source_id)
descr += "<hr>"
descr += "<li><small><b>created at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.created_at)) if self.created_at else "")
descr += "<li><small><b>updated at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.updated_at)) if self.updated_at else "")
descr += "</ol>"
descr += "</html>"
return descr
class FileDescriptor():
def __init__(self, filename, format, version, created_at, updated_at, size) -> None:
super().__init__()
self.name = filename
self.size = None
self.format = format
self.version = version
self.created_at = created_at
self.updated_at = updated_at
self.size = size
self.block_count = 0
self.data_array_count = 0
self.tag_count = 0
self.group_count = 0
self.data_frame_count = 0
def toHtml(self):
def namAndPath(filename):
parts = filename.split(os.sep)
name = parts[-1]
path = ""
if len(parts) > 1:
path = os.sep.join(parts[:-1])
return name, path
name, path = namAndPath(self.name)
descr = "<html><h4>%s</h4>" % name
descr += "<ol style='list-style-type:none'>"
descr += "<li><small><b>location:</b> %s</small></li>" % (path if len(path) > 1 else ".")
descr += "<li><small><b>format:</b> %s</small></li>" % (self.format)
descr += "<li><small><b>nix format version:</b> %s</small></li>" % (str(self.version))
descr += "<li><small><b>file size:</b> %.2f MB</small></li>" % (self.size)
descr += "<hr>"
descr += "<li>File contents</li>"
descr += "<li><small><b>blocks:</b> %i</small></li>" % self.block_count
descr += "<li><small><b>groups:</b> %i</small></li>" % self.group_count
descr += "<li><small><b>data arrays:</b> %i</small></li>" % self.data_array_count
descr += "<li><small><b>data frames:</b> %i</small></li>" % self.data_frame_count
descr += "<li><small><b>tags:</b> %i</small></li>" % self.tag_count
descr += "<hr>"
descr += "<li><small><b>created at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.created_at)))
descr += "<li><small><b>updated at:</b> %s</small></li>" % (str(dt.datetime.fromtimestamp(self.updated_at)))
descr += "</ol>"
descr += "</html>"
return descr
class NodeType(Enum):
Root = "root"
Section = "Section"
Block = "Block"
DataArray = "Data Array"
DataFrame = "Data Frame"
Property = "Property"
Dimension = "Dimension"
Source = "Source"
Tag = "Tag"
MultiTag = "Multi Tag"
Group = "Group"
Feature="Feature"
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class EntityBuffer():
def __init__(self) -> None:
super().__init__()
self._buffer = {}
def put(self, entity):
if not hasattr(entity, "id"):
return
id = entity.id
if id not in self._buffer.keys():
self._buffer[id] = entity
def has(self, id):
return id in self._buffer.keys()
def get(self, id):
if self.has(id):
return self._buffer[id]
else:
return None
def clear(self):
self._buffer.clear()
class FileHandler(metaclass=Singleton):
def __init__(self) -> None:
super().__init__()
self._filename = None
self._nix_file = None
self._file_requests = []
self._entity_buffer = EntityBuffer()
self._file_descriptor = None
self._file_version = None
def open(self, filename):
self.close()
if not os.path.exists(filename):
return False, "File %s could not be found!" % filename
try:
self._nix_file = nix.File.open(filename, nix.FileMode.ReadOnly)
self._filename = filename
self._file_descriptor = FileDescriptor(self.filename, self._nix_file.format, self._nix_file.version,
self._nix_file.created_at, self._nix_file.updated_at, os.path.getsize(self.filename)/1e+6)
self.file_descriptor.block_count = len(self._nix_file.blocks)
for b in self._nix_file.blocks:
self.file_descriptor.data_array_count += len(b.data_arrays)
self.file_descriptor.group_count += len(b.groups)
self.file_descriptor.tag_count += len(b.tags)
self.file_descriptor.tag_count += len(b.multi_tags)
if hasattr(b, "data_frames"):
self.file_descriptor.data_frame_count += len(b.data_frames)
self._file_version = self._nix_file.version
return True, "Successfully opened file %s." % filename.split(os.sep)[-1]
except RuntimeError as e:
return False, "Failed to open file %s! \n Error message is: %s" % (filename, e)
except OSError as e:
return False, "Failed to open file %s! \n Error message is: %s\n Probably no nix file?!" % (filename, e)
def close(self):
if self._nix_file is not None and self._nix_file.is_open():
self._nix_file.close()
self._nix_file = None
self._file_requests = []
self._entity_buffer.clear()
self._file_descriptor = None
self._file_version = None
@property
def file_descriptor(self):
return self._file_descriptor
@property
def is_valid(self):
return self._nix_file is not None and self._nix_file.is_open()
@property
def filename(self):
return self._filename
def request_section_descriptor(self, id):
fs = self._entity_buffer.get(id)
if fs is None:
found_section = self._nix_file.find_sections(lambda s: s.id == id)
fs = found_section[0] if len(found_section) > 0 else None
if fs is None:
return None
else:
item = ItemDescriptor(fs.name, fs.id, fs.type, definition=fs.definition, entity_type=NodeType.Section)
return item
def request_metadata(self, root_id=None, depth=1):
"""[summary]
Args:
root_id ([type], optional): [description]. Defaults to None.
depth (int, optional): [description]. Defaults to 1.
"""
def get_subsections(section):
sub_sections = []
for s in section.sections:
self._entity_buffer.put(s)
sub_sections.append(ItemDescriptor(s.name, s.id, s.type, definition=s.definition, entity_type=NodeType.Section))
return sub_sections
def get_properties(section):
props = []
for p in section.props:
value = ""
if self._file_version < (1,1,1):
vals = p.values
if len(vals) > 1:
value += "["
value += ",".join(map(str, [v.value for v in vals]))
value += "]"
else:
value = str(vals[0].value)
else:
vals = p.values
value += "["
value += ",".join(map(str, [v.value for v in vals]))
value += "]"
if p.unit is not None:
value += " " + p.unit
props.append(ItemDescriptor(p.name, p.id, value=value, entity_type=NodeType.Property))
return props
sections = []
properties = []
if root_id is None:
sections = get_subsections(self._nix_file)
else:
fs = self._entity_buffer.get(root_id)
if fs is None:
found_section = self._nix_file.find_sections(lambda s: s.id == root_id)
fs = found_section[0] if len(found_section) > 0 else None
if fs is None:
return sections, properties
sections.extend(get_subsections(fs))
properties.extend(get_properties(fs))
return sections, properties
def _entity_info(self, entities, block_id, entity_type):
infos = []
for e in entities:
self._entity_buffer.put(e)
itd = ItemDescriptor(e.name, e.id, e.type, definition=e.definition, entity_type=entity_type, block_id=block_id)
section = e.metadata if hasattr(e, "metadata") else None
itd.metadata_id = section.id if section is not None else None
itd.data_type = e.data_type if hasattr(e, "data_type") else None
itd.created_at = e.created_at if hasattr(e, "created_at") else None
itd.updated_at = e.updated_at if hasattr(e, "updated") else None
itd.shape = e.shape if hasattr(e, "shape") else None
src = e.source if hasattr(e, "source") else None
itd.source_id = src.id if src is not None else None
infos.append(itd)
# TODO set the value to something meaningful for the various entity types
return infos
def request_blocks(self):
return self._entity_info(self._nix_file.blocks, None, NodeType.Block)
def get_block(self, id):
b = b = self._entity_buffer.get(id)
if not b:
b = self._nix_file.blocks[id]
return b
def request_data_arrays(self, block_id):
b = self.get_block(block_id)
return self._entity_info(b.data_arrays, block_id, NodeType.DataArray)
def request_tags(self, block_id):
b = self.get_block(block_id)
tags = self._entity_info(b.tags, block_id, NodeType.Tag)
tags.extend(self._entity_info(b.multi_tags, block_id, NodeType.MultiTag))
return tags
def request_references(self, block_id, tag_id, is_mtag):
b = self.get_block(block_id)
t = self._entity_buffer.get(tag_id)
if t is None:
if is_mtag:
t = b.multi_tags[tag_id]
else:
t = b.tags[tag_id]
return self._entity_info(t.references, block_id, NodeType.DataArray)
def request_features(self, block_id, tag_id, is_mtag):
b = self.get_block(block_id)
t = self._entity_buffer.get(tag_id)
if t is None:
if is_mtag:
t = b.multi_tags[tag_id]
else:
t = b.tags[tag_id]
feats = []
for f in t.features:
itd = ItemDescriptor(f.data.name, f.id, f.link_type, definition=f.data.definition, block_id=block_id, entity_type=NodeType.Feature)
feats.append(itd)
return feats
def request_dimensions(self, block_id, array_id):
da = self._entity_buffer.get(array_id)
if da is None:
b = self.get_block(block_id)
da = b.data_arrays[array_id]
dimensions = []
for i, d in enumerate(da.dimensions):
dim_name = "%i. dim: %s" % (i+1, d.label if hasattr(d, "label") else "")
dim_type= "%s %s" % (d.dimension_type, "dimension")
dimensions.append(ItemDescriptor(dim_name, type=dim_type, entity_type=NodeType.Dimension, block_id=block_id))
return dimensions
def request_data_frames(self, block_id):
if self._nix_file.version[1] >= 2:
b = self.get_block(block_id)
return self._entity_info(b.data_frames, block_id, NodeType.DataFrame)
return []
def request_groups(self, block_id):
b = self.get_block(block_id)
return self._entity_info(b.groups, block_id, NodeType.Group)
def request_sources(self, block_id, parent_source_id=None):
def get_subsources(src):
sub_sources = []
for s in src.sources:
self._entity_buffer.put(s)
sub_sources.append(ItemDescriptor(s.name, s.id, s.type, definition=s.definition, entity_type=NodeType.Source))
return sub_sources
b = self.get_block(block_id)
if parent_source_id is None:
return self._entity_info(b.sources, block_id, NodeType.Source)
else:
srcs = b.find_sources(lambda s: s.id == parent_source_id)
sources = []
for src in srcs:
sources.extend(get_subsources(src))
return sources

147
nixview/main_window.py Normal file
View File

@@ -0,0 +1,147 @@
import sys
from PyQt5.QtWidgets import QWidget, QFileDialog, QMainWindow, QMenuBar, QToolBar, QAction, QStatusBar, QSizePolicy
from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import QSize, QSettings, Qt
from file_handler import FileHandler, NodeType
import constants as cnst
import communicator as comm
from central_widget import CentralWidget
class NixView(QMainWindow):
def __init__(self, *args, **kwargs):
super(NixView, self).__init__(*args, **kwargs)
self._current_item = None
self.setWindowTitle("NixView")
self.setWindowIcon(QIcon('./icons/nixview.icns'))
self._file_handler = FileHandler()
self.setStatusBar(QStatusBar(self))
self.setMenuBar(QMenuBar(self))
self.create_actions()
self._cw = CentralWidget(self)
self.setCentralWidget(self._cw)
comm.communicator.open_recent.connect(self.on_open_recent)
comm.communicator.item_selected.connect(self.on_item_selected)
self.show()
def on_open_recent(self, event):
self.open_file(event)
def on_item_selected(self, current):
self._current_item = current
current_item_type = current._node_descriptor.entity_type
enable = current_item_type == NodeType.MultiTag or current_item_type == NodeType.DataArray or current_item_type == NodeType.Tag
self._plot_action.setEnabled(enable)
self._table_action.setEnabled(enable)
def create_actions(self):
self._file_open_action = QAction(QIcon("./icons/nix_open.png"), "Open", self)
self._file_open_action.setStatusTip("Open nix file")
self._file_open_action.setShortcut(QKeySequence("Ctrl+o"))
self._file_open_action.triggered.connect(self.on_file_open)
self._file_close_action = QAction(QIcon("./icons/nix_close.png"), "Close", self)
self._file_close_action.setStatusTip("Close current nix file")
self._file_close_action.setShortcut(QKeySequence("Ctrl+w"))
self._file_close_action.setEnabled(False)
self._file_close_action.triggered.connect(self.on_file_close)
self._quit_action = QAction(QIcon("./icons/quit.png"), "Quit", self)
self._quit_action.setStatusTip("Close current file and quit")
self._quit_action.setShortcut(QKeySequence("Ctrl+q"))
self._quit_action.triggered.connect(self.on_quit)
self._plot_action = QAction(QIcon("./icons/nix_plot.png"), "Plot", self)
self._plot_action.setStatusTip("Plot currently selected entity")
self._plot_action.setShortcut(QKeySequence("Ctrl+p"))
self._plot_action.setEnabled(False)
self._plot_action.triggered.connect(self.on_item_plot)
self._table_action = QAction(QIcon("./icons/nix_table.png"), "Show table", self)
self._table_action.setStatusTip("Show data as table")
self._table_action.setShortcut(QKeySequence("Ctrl+t"))
self._table_action.setEnabled(False)
# self._table_action.triggered.connect(self.on_file_close)
self.create_toolbar()
self.create_menu()
def create_toolbar(self):
self._toolbar = QToolBar("My main toolbar")
#self._toolbar.setStyleSheet("QToolButton:!hover {background-color:none}")
self._toolbar.setAllowedAreas(Qt.LeftToolBarArea | Qt.TopToolBarArea)
self._toolbar.setIconSize(QSize(32, 32))
self._toolbar.addAction(self._file_open_action)
self._toolbar.addAction(self._file_close_action)
self._toolbar.addSeparator()
self._toolbar.addAction(self._plot_action)
self._toolbar.addAction(self._table_action)
empty = QWidget()
empty.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._toolbar.addWidget(empty)
self._toolbar.addSeparator()
self._toolbar.addAction(self._quit_action)
self.addToolBar(Qt.LeftToolBarArea, self._toolbar)
def create_menu(self):
menu = self.menuBar()
file_menu = menu.addMenu("&File")
file_menu.addAction(self._file_open_action)
file_menu.addAction(self._file_close_action)
file_menu.addSeparator()
file_menu.addAction(self._quit_action)
plot_menu = menu.addMenu("&Plot")
plot_menu.addAction(self._plot_action)
plot_menu.addAction(self._table_action)
self.setMenuBar(menu)
def _update_recent_files(self, filename):
settings = QSettings(cnst.organization, cnst.application)
recent_file_max_count = settings.value(cnst.settings_recent_file_max_count_key, 10, type=int)
filenames = settings.value(cnst.settings_recent_files_key, [])
new_filenames = [filename]
if filename in filenames:
del filenames[filenames.index(filename)]
new_filenames.extend(filenames)
settings.setValue(cnst.settings_recent_files_key, new_filenames[:recent_file_max_count])
del settings
def open_file(self, filename):
self._current_item = None
success, msg = self._file_handler.open(filename)
self.statusBar().showMessage(msg, 5000)
if success:
self._file_close_action.setEnabled(success)
self._cw.show_file_content()
self._update_recent_files(filename)
def on_file_open(self, s):
dlg = QFileDialog(self, 'Open nix data file', '', "NIX files (*.h5 *.nix)")
dlg.setFileMode(QFileDialog.ExistingFile)
filenames = None
if dlg.exec_():
filenames = dlg.selectedFiles()
self.open_file(filenames[0])
def on_file_close(self, s):
self._file_handler.close()
self._cw.reset()
self._current_item = None
self._file_close_action.setEnabled(False)
self.statusBar().showMessage("Successfully closed current file", 1000)
def on_quit(self, s):
self._file_handler.close()
sys.exit()
def on_item_plot(self, s):
if self._current_item is not None:
self._cw.plot_item(self._current_item)

21
nixview/nixview.py Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/python3
import sys
from PyQt5.QtWidgets import QApplication
from main_window import NixView
import argparse
def main():
parser = argparse.ArgumentParser(description="NixView. Viewer for NIX data files. For more on nix see https://nixio.readthedocs.io/en/master")
parser.add_argument("file", nargs="?", default="", type=str, help="The nix file that should be opened.")
args = parser.parse_args()
app = QApplication(sys.argv)
window = NixView()
window.setMinimumWidth(800)
window.setMinimumHeight(600)
if len(args.file.strip()) > 0:
window.open_file(args.file)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

36
nixview/plot_screen.py Normal file
View File

@@ -0,0 +1,36 @@
from PyQt5.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import QObject, pyqtSignal, Qt
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
class PlotScreen(QWidget):
close_signal = pyqtSignal()
def __init__(self, parent) -> None:
super().__init__(parent=parent)
sc = MplCanvas(self, width=5, height=4, dpi=100)
sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])
self.setLayout(QVBoxLayout())
self.layout().addWidget(sc)
close_btn = QPushButton("close")
close_btn.clicked.connect(self.on_close)
self.layout().addWidget(close_btn)
def on_close(self):
self.close_signal.emit()
def plot(self, item):
print("plot!", item)

384
nixview/tree_model.py Normal file
View File

@@ -0,0 +1,384 @@
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QTreeView, QTreeWidgetItem, QAbstractItemView, QHeaderView
from enum import Enum
from file_handler import ItemDescriptor, NodeType
column_names = ['Name', 'Type', 'Value', 'Description', 'ID',]
class TreeType(Enum):
Full = "full view"
Metadata = "metadata view"
Data = "data view"
class NixTreeItem(QTreeWidgetItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(parent)
self._node_descriptor = node_descriptor
self._file_handler = file_handler
self._parent_item = parent
self._child_items = []
self._is_loaded = False
@property
def node_descriptor(self):
return self._node_descriptor
@property
def entity_type(self):
return self._node_descriptor.entity_type
def child(self, row):
if row < len(self._child_items):
return self._child_items[row]
return None
def childCount(self):
n = 0 if self._is_loaded else 1
return max(n, len(self._child_items))
def columnCount(self):
return len(column_names)
def data(self, column):
if column == 0:
return self._node_descriptor.name
elif column == 1:
return self._node_descriptor.type
elif column == 2:
return self._node_descriptor.value
elif column == 3:
return self._node_descriptor.definition
elif column == 4:
return self._node_descriptor.id
else:
return None
def parent(self):
return self._parent_item
def row(self):
if self._parent_item:
return self._parent_item._child_items.index(self)
return 0
class FileTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler=file_handler, parent=parent)
self._is_loaded = False
def load_children(self):
self._child_items = []
sections, _ = self._file_handler.request_metadata()
for s in sections:
self._child_items.append(SectionTreeItem(s, self._file_handler, parent=self))
blocks = self._file_handler.request_blocks()
for b in blocks:
self._child_items.append(BlockTreeItem(b, self._file_handler, parent=self))
self._is_loaded = True
class DataTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler=file_handler, parent=parent)
self._is_loaded = False
def load_children(self):
self._child_items = []
blocks = self._file_handler.request_blocks()
for b in blocks:
self._child_items.append(BlockTreeItem(b, self._file_handler, parent=self))
self._is_loaded = True
class MetadataTreeItem(NixTreeItem):
"""
Root item for a metadata tree.
Args:
"""
def __init__(self, node_descriptor, file_handler, parent=None, root_section_id=None):
super().__init__(node_descriptor, file_handler=file_handler, parent=parent)
self._root_section_id = root_section_id
self._is_loaded = False
def load_children(self):
self._child_items = []
if self._root_section_id is not None:
item = self._file_handler.request_section_descriptor(self._root_section_id)
if item is not None:
self._child_items.append(SectionTreeItem(item, self._file_handler, parent=self))
else:
sections, _ = self._file_handler.request_metadata(root_id=self._root_section_id)
for s in sections:
self._child_items.append(SectionTreeItem(s, self._file_handler, parent=self))
self._is_loaded = True
class BlockTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable |Qt.ItemIsEditable)
self._is_loaded = False
def load_children(self):
self._child_items = []
arrays = self._file_handler.request_data_arrays(self._node_descriptor.id)
for a in arrays:
self._child_items.append(DataArrayTreeItem(a, self._file_handler, parent=self))
for t in self._file_handler.request_tags(self._node_descriptor.id):
self._child_items.append(TagTreeItem(t, self._file_handler, parent=self))
data_frames = self._file_handler.request_data_frames(self._node_descriptor.id)
for df in data_frames:
self._child_items.append(DataFrameTreeItem(df, self._file_handler, parent=self))
sources = self._file_handler.request_sources(self._node_descriptor.id)
for s in sources:
self._child_items.append(SourceTreeItem(s, self._file_handler, parent=self))
self._is_loaded = True
class SourceTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = False
def load_children(self):
sources = self._file_handler.request_sources(self._node_descriptor.block_id, self._node_descriptor.id)
for s in sources:
self._child_items.append(SourceTreeItem(s, self._file_handler, parent=self))
self._is_loaded = True
class GroupTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = False
def load_children(self):
self._child_items = []
arrays = self._file_handler.request_data_arrays(self._node_descriptor.id)
for a in arrays:
self._child_items.append(DataArrayTreeItem(a, self._file_handler, parent=self))
for t in self._file_handler.request_tags(self._node_descriptor.id):
self._child_items.append(TagTreeItem(t, self._file_handler, parent=self))
data_frames = self._file_handler.request_data_frames(self._node_descriptor.id)
for df in data_frames:
self._child_items.append(DataFrameTreeItem(df, self._file_handler, parent=self))
sources = self._file_handler.request_sources(self._node_descriptor.id)
for s in sources:
self._child_items.append(SourceTreeItem(s, self._file_handler, parent=self))
self._is_loaded = True
class DataFrameTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = True
class FeatureTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = True
class DataArrayTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = False
def load_children(self):
self._child_items = []
dimensions = self._file_handler.request_dimensions(self._node_descriptor.block_id, self._node_descriptor.id)
for d in dimensions:
self._child_items.append(DimensionTreeItem(d, self._file_handler, parent=self))
self._is_loaded = True
class DimensionTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = True
class SectionTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler=file_handler, parent=parent)
self.setFlags(Qt.ItemIsSelectable)
self._is_loaded = False
def load_children(self):
self._child_items = []
sections, properties = self._file_handler.request_metadata(self._node_descriptor.id)
for s in sections:
self._child_items.append(SectionTreeItem(s, self._file_handler, parent=self))
for p in properties:
self._child_items.append(PropertyTreeItem(p, self._file_handler, parent=self))
self._is_loaded = True
class PropertyTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = True
def childCount(self):
return 0
class TagTreeItem(NixTreeItem):
def __init__(self, node_descriptor, file_handler, parent=None):
super().__init__(node_descriptor, file_handler, parent=parent)
self._is_loaded = False
def load_children(self):
self._child_items = []
references = self._file_handler.request_references(self._node_descriptor.block_id, self._node_descriptor.id, self._node_descriptor.entity_type == NodeType.MultiTag)
for r in references:
self._child_items.append(DataArrayTreeItem(r, self._file_handler, self))
features = self._file_handler.request_features(self._node_descriptor.block_id, self._node_descriptor.id, self._node_descriptor.entity_type == NodeType.MultiTag)
for f in features:
self._child_items.append(FeatureTreeItem(f, self._file_handler, self))
self._is_loaded = True
class TreeModel(QAbstractItemModel):
def __init__(self, file_handler, tree_type=TreeType.Full, parent=None, root_section_id=None):
super(TreeModel, self).__init__(parent)
nd = ItemDescriptor(file_handler.filename, type="Root item")
self.type_icons = {NodeType.Block: QIcon("./icons/nix_block_1d.png"),
NodeType.Source: QIcon("./icons/nix_source.png"),
NodeType.DataArray: QIcon("./icons/nix_data_array.png"),
NodeType.Dimension: QIcon("./icons/nix_dimension.png"),
NodeType.DataFrame: QIcon("./icons/nix_data_frame.png"),
NodeType.Section: QIcon("./icons/nix_section.png"),
NodeType.Property: QIcon("./icons/nix_property.png"),
NodeType.Tag: QIcon("./icons/nix_tag.png"),
NodeType.MultiTag: QIcon("./icons/nix_tag.png"),
NodeType.Group: QIcon("./icons/nix_group.png"),
NodeType.Feature: QIcon("./icons/nix_feature.png")}
if tree_type == TreeType.Full:
self.root_item = FileTreeItem(nd, file_handler, parent=None)
elif tree_type == TreeType.Metadata:
self.root_item = MetadataTreeItem(nd, file_handler, parent=None, root_section_id=root_section_id)
else:
self.root_item = DataTreeItem(nd, file_handler, parent=None)
self.root_item.load_children()
def columnCount(self, parent):
return len(column_names)
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.data(index.column())
elif role == Qt.DecorationRole and index.column() == 0:
if item.entity_type in self.type_icons.keys():
return self.type_icons[item.entity_type]
else:
return None
else:
return None
def canFetchMore(self, index):
if not index.isValid():
return False
item = index.internalPointer()
return not item._is_loaded
def fetchMore(self, index):
item = index.internalPointer()
item.load_children()
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return column_names[section]
return None
def index(self, row, column, parent_index):
idx = QModelIndex()
if not self.hasIndex(row, column, parent_index):
return idx
if not parent_index.isValid():
parentItem = self.root_item
else:
parentItem = parent_index.internalPointer()
childItem = parentItem.child(row)
if childItem:
idx = self.createIndex(row, column, childItem)
return idx
def parent(self, child_index):
if not child_index.isValid():
return QModelIndex()
child_item = child_index.internalPointer()
parent_item = child_item.parent()
if parent_item is None:
return QModelIndex()
# return self.createIndex(0, 0, self.root_item)
return self.createIndex(parent_item.row(), 0, parent_item)
def rowCount(self, parent_index):
if parent_index.column() > 0:
return 0
if not parent_index.isValid():
parentItem = self.root_item
else:
parentItem = parent_index.internalPointer()
return parentItem.childCount()
class NixTreeView(QTreeView):
icon_size = QSize(30, 30)
def __init__(self, parent=None) -> None:
super().__init__(parent=parent)
self.expanded.connect(self.columnResize)
self.collapsed.connect(self.columnResize)
self.setAlternatingRowColors(True)
self.setUniformRowHeights(True) # Allows for scrolling optimizations.
self.setWindowTitle("Data Tree")
self.setIconSize(self.icon_size)
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setSelectionMode(QAbstractItemView.SingleSelection)
header = self.header()
header.setStretchLastSection(True)
header.setFirstSectionMovable(False)
header.setSectionResizeMode(QHeaderView.ResizeToContents)
def columnResize(self, index):
for i in range(len(column_names)):
self.resizeColumnToContents(i)