Source code for tools.project.metadataManager
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
""" Module metadataManager:
This module contains classes for opening a metadata manager to manage the
metadata on the current project in Mari.
"""
# noinspection PyUnresolvedReferences
import mari
import logging
import traceback
import os
from PySide.QtGui import QTableWidgetItem, QHeaderView, QMessageBox, QFileDialog
from PySide import QtCore
from stkMariTools.lib.jsonutils import JSONUtils
from stkMariTools.lib.ui_utils import MariToolsMenuItem, MariWidget
from stkMariTools.tools.project.ui.metadataManagerUi import Ui_Form_metadataManager
[docs]class MetadataManagerMenuItem(MariToolsMenuItem):
"""
This class adds the Metadata Manager action.
"""
def __init__( self ):
"""
The constructor.
:return:
"""
# Call base constructor
super( MetadataManagerMenuItem, self ).__init__()
# Set the class instance in order for the action command to be
# able to be set in the namespace correctly.
mari.MetadataManagerMenuItem = self
# Action item is to be called in the Menu
self.actionIdentifier = 'Metadata Manager'
# Python command to be run when action is executed from the Menu
self.actionCommand = 'mari.MetadataManagerMenuItem.openUi()'
# Path to the action in the Mari Menu
self.actionPath = 'MainWindow/&Scripts/&Project'
# Icon to use for the action
self.actionIcon = 'About'
# Register the plugin
self.addMariToolsMenuItem()
[docs]class MetadataManager(MariWidget):
"""
This class creates a Metadata Manager to manage metadata on Mari projects.
"""
def __init__(self, title='Metadata Manager'):
"""
The constructor.
:param title:
:return:
"""
# Create class logger
self.logger = logging.getLogger(__name__)
self.ui_form = None
# Define table headers
self.headers = ['Name', 'Data']
# Define default metadata keys
self.default_keys = [
'AnimControlFrame',
'ColorEnabled',
'ColorProfile',
'MriImagePickerImageImage',
'MriImportPath',
'MriImportTemplate',
'OcioColorSpace',
'OcioConfigPath',
'OcioDisplay',
'OcioExtrapolate',
'OcioGain',
'OcioGamma',
'OcioLutPath',
'OcioSwizzle',
'OcioView'
]
# Store all current metadata as backup
self.default_metadata = self.getMetadata()
# Call base constructor
super( MetadataManager, self ).__init__(title=title)
[docs] def defineUi(self):
"""
This method should be overloaded to handle defining the actual UI interface.
It is run before populateData() and makeConnections().
:return:
"""
# Get instance of UI form and setup widgets onto that instance
self.ui_form = Ui_Form_metadataManager()
self.ui_form.setupUi(self.widget_holder)
self.ui_form.tableWidget_metadataView.setColumnCount(2)
# Create horizontal header
horizontal_header = QHeaderView(QtCore.Qt.Orientation.Horizontal)
# Set table headers properties
self.ui_form.tableWidget_metadataView.setHorizontalHeader(
horizontal_header
)
self.ui_form.tableWidget_metadataView.setHorizontalHeaderLabels(self.headers)
# First re-size the header to the contents, then allow user to resize further
horizontal_header.setResizeMode(QHeaderView.ResizeToContents)
horizontal_header.setResizeMode(QHeaderView.Interactive)
horizontal_header.setStretchLastSection(True)
[docs] def makeConnections(self, *args, **kwargs):
"""
This method handles connecting UI widgets to callbacks.
:param args:
:param kwargs:
:return: ``None``
"""
# Connect Mari scene open/close events to metadata refresh
mari.utils.connect(mari.projects.opened, self.refreshMetadata)
mari.utils.connect(mari.projects.archived, self.refreshMetadata)
mari.utils.connect(mari.projects.closed, self.refreshMetadata)
metadataView_model = self.ui_form.tableWidget_metadataView.model()
metadataView_model.dataChanged.connect(self.setMetadata)
# Connect UI buttons
self.ui_form.btn_reset.clicked.connect(self.resetToDefault)
self.ui_form.btn_removeCustomMetadata.clicked.connect(self.removeNonStandardMetadata)
self.ui_form.btn_refresh.clicked.connect(self.refreshMetadata)
self.ui_form.btn_addMetadata.clicked.connect(self.addMetadataEntry)
self.ui_form.btn_exportMetadata.clicked.connect(self.exportMetadataToFile)
self.ui_form.btn_importMetadata.clicked.connect(self.importMetadataFromFile)
# Close button
self.ui_form.btn_close.clicked.connect(self.cancelAction)
[docs] def populateData(self, *args, **kwargs):
"""
This method populates UI with data.
:param args:
:param kwargs:
:return:
"""
current_project = mari.projects.current()
# Refresh the table view
self.refreshMetadata(current_project)
[docs] def refreshMetadata(self, project=None):
"""
This method populates/refreshes the metadata view with data.
:param project:
:return:
"""
self.logger.debug('Refresh triggered...')
# Temporarily disconnect the metadata view dataChanged() signal so
# that updates are not needlessly called on the project
metadataView_model = self.ui_form.tableWidget_metadataView.model()
# mari.utils.disconnect(metadataView_model.dataChanged, self.setMetadata)
if not project:
project = mari.projects.current()
if not project:
self.logger.warning('# No project could be found to grab metadata from!')
return
# Reset default metadata store if none exists
# todo: this may cause opening new project to keep metadata from old session
if not self.default_metadata:
self.default_metadata = self.getMetadata(project)
# Set the current project label
self.ui_form.lbl_projectName.setText(project.name())
metadata = {}
# Grab metadata values and format dictionary with names/data
for key in project.metadataNames():
metadata[key] = project.metadata(key)
for i, key in enumerate(sorted(metadata.keys())):
self.ui_form.tableWidget_metadataView.setRowCount(len(metadata.keys()))
key_item = QTableWidgetItem(key)
value_item = QTableWidgetItem(metadata[key])
self.ui_form.tableWidget_metadataView.setItem(i, 0, key_item)
self.ui_form.tableWidget_metadataView.setItem(i, 1, value_item)
# Resize the columns of the tableview
self.ui_form.tableWidget_metadataView.resizeColumnsToContents()
# Auto resize window and tablewidget to fit metadata
header_width = self.ui_form.tableWidget_metadataView.\
horizontalHeader().length()
self.widget_holder.setMinimumWidth(header_width + 80)
# Reconnect the metadata view dataChanged() signal
metadataView_model.dataChanged.connect(self.setMetadata)
self.logger.debug('Successfully refreshed all metadata!')
[docs] def getMetadata(self, project=None):
"""
This method gets the current metadata on the project and returns it as a
``dict`` object.
:param project:
:return: ``dict`` containing the current metadata available in the project.
:rtype: ``dict``
"""
if not project:
project = mari.projects.current()
if not project:
self.logger.error('### No project is currently available to get '
'metadata from!!!')
return
# Check that there is at least one metadata key on project
if project.metadataNames():
metadata = {}
for name in project.metadataNames():
metadata[name] = project.metadata(name)
return metadata
else:
self.logger.warning('# There is no metadata on this object!')
[docs] def resetToDefault(self, project=None, keep_non_standard_entries=True):
"""
This method resets all metadata to the original state that the project
was saved in when first loaded.
:param project: ``Project`` instance of Mari ``ProjectManager``
:param keep_non_standard_entries: ``bool`` that determines if added metadata
entries should be removed
:return: ``None``
"""
self.logger.debug('Resetting metadata values...')
if not project:
project = mari.projects.current()
if not project:
self.logger.error('### No project is currently available to '
'remove metadata from!!!')
return
# Warn user prompt
prompt = mari.utils.messageResult(
'This operation is not undo-able! Are you sure you want to proceed?',
'Confirm resetting metadata?',
QMessageBox.StandardButton.Ok|QMessageBox.Cancel,
QMessageBox.Icon.Warning
)
if prompt == QMessageBox.StandardButton.Ok:
if self.default_metadata:
for key, value in self.default_metadata.items():
# Set default values and keys for metadata
self.logger.debug('Setting: {0} to {1}...'.format(key, value))
project.setMetadata(key, value)
if not keep_non_standard_entries:
self.removeNonStandardMetadata(project)
else:
self.logger.warning('# There was no default metadata that could be found!')
# Refresh UI
self.refreshMetadata()
self.logger.info('Successfully reset metadata to default values!')
[docs] def removeNonStandardMetadata(self, project=None):
"""
This method removes all non-standard metadata entries from the given project.
:param project:
:return:
"""
if not project:
project = mari.projects.current()
if not project:
self.logger.error('### No project is currently available '
'to remove metadata from!!!')
return
if not self.default_metadata:
self.logger.error('### There is no default metadata found on project!!!')
return
# Warn user prompt
prompt = mari.utils.messageResult(
'This operation is not undo-able! Are you sure you want to proceed?',
'Confirm removing non-standard metadata?',
QMessageBox.StandardButton.Ok|QMessageBox.Cancel,
QMessageBox.Icon.Warning
)
if prompt == QMessageBox.StandardButton.Ok:
# Remove all extra keys
for item in project.metadataNames():
if item not in self.default_keys:
self.logger.debug('Removing metadata: {0}'.format(item))
project.removeMetadata(item)
# Refresh the UI
self.refreshMetadata()
[docs] def setMetadata(self):
"""
This method is called whenever the user changes the data in the metadata view
and acts to apply the changes to the actual Mari project metadata.
:return:
"""
if not self.checkForOpenProject():
return
# Read in values from the metadata view and apply it to the project
tableView_model = self.ui_form.tableWidget_metadataView.model()
if not tableView_model or tableView_model.rowCount() == 0:
return
current_project = mari.projects.current()
for i in range(tableView_model.rowCount()):
key = tableView_model.data(tableView_model.index(i, 0))
value = tableView_model.data(tableView_model.index(i, 1))
# Convert value to boolean if specific string detected
if value == 'true' or value == 'True':
value = True
elif value == 'false' or value == 'False':
value = False
# Check for invalid data types
if not key or value == None:
continue
try:
current_project.setMetadata(key, value)
except ValueError:
self.logger.error('### Unable to set: {0} to {1}!!!\n{2}'
.format(key, value, traceback.print_exc()))
raise ValueError
[docs] def addMetadataEntry(self):
"""
This method adds a new metadata entry to the metadata view.
:return: ``None``
"""
self.logger.debug('Adding new metadata entry...')
row_count = self.ui_form.tableWidget_metadataView.rowCount()
self.ui_form.tableWidget_metadataView.insertRow(row_count)
[docs] def exportMetadataToFile(self):
"""
This method exports the current project metadata to a serialized file
that can be loaded and applied to other projects.
:return:
"""
self.logger.debug('Exporting metadata to file...')
if not self.checkForOpenProject():
return
# Form default directory for exporting files to
default_export_path = os.path.join(
os.path.expanduser('~'),
'Mari',
'Exported Metadata'
)
if not os.path.isdir(default_export_path):
res = QMessageBox.question(
self,
'Create default directory?',
'Create a default location to save metadata files to?',
QMessageBox.Ok|QMessageBox.Cancel
)
if res == QMessageBox.Ok:
os.makedirs(default_export_path)
# Open file dialog for user to choose where to export file to
export_path = QFileDialog.getSaveFileName(
self,
'Choose location to save file...',
default_export_path,
'JSON Metadata files(*.json)'
)[0]
if not export_path:
return
# Read metadata from the project
current_project = mari.projects.current()
if not current_project:
return
metadata_items = current_project.metadataNames()
if metadata_items:
metadata = {}
for name in metadata_items:
metadata[name] = current_project.metadata(name)
# Export the file to the specified location
JSONUtils.write(export_path, metadata)
self.logger.info('Successfully exported to: {0}'.format(export_path))
[docs] def importMetadataFromFile(self):
"""
This method imports an existing metadata file and applies the data to the
current project.
:return:
"""
self.logger.debug('Importing metadata from file...')
# Temporarily disconnect the metadata view dataChanged() signal so
# that updates are not needlessly called on the project
metadataView_model = self.ui_form.tableWidget_metadataView.model()
mari.utils.disconnect(metadataView_model.dataChanged, self.setMetadata)
if not self.checkForOpenProject():
return
current_project = mari.projects.current()
# Form default directory for importing files from
default_import_path = os.path.join(
os.path.expanduser('~'),
'Mari',
'Exported Metadata'
)
# Open file dialog for user to choose where to import file to
import_path = QFileDialog.getOpenFileName(
self,
'Choose metadata file to iport...',
default_import_path,
'*.json'
)[0]
if not import_path:
return
if not os.path.isfile(import_path):
self.logger.error('### {0} does not exist!!!'.format(import_path))
QMessageBox.warning(
self,
'File does not exist!',
'{0} does not exist!\nPlease select a different file and try again!'
.format(import_path)
)
return
# Read in metadata from the file
metadata = JSONUtils.read(import_path)
# Apply metadata to the project
for key, value in metadata.items():
self.logger.debug('Setting: {0} to {1}'.format(key, value))
current_project.setMetadata(key, value)
self.refreshMetadata()
# Reconnect the metadata view dataChanged() signal
metadataView_model.dataChanged.connect(self.setMetadata)
self.logger.info('Successfully imported: {0}!'.format(import_path))
[docs] def checkForOpenProject(self):
"""
This method checks if a project is currently open in Mari.
:return: ``bool`` indicating if project is open in Mari.
:rtype: ``bool``
"""
# Get latest project currently open
current_project = mari.projects.current()
if not current_project:
self.logger.error('### There is no project currently open!!!')
QMessageBox.warning(
self,
'No project open!',
'There is no Mari project currently open!'
)
return False
else:
return True