diff --git a/beat/editor/widgets/assetwidget.py b/beat/editor/widgets/assetwidget.py index 71b1c14fe2d0356dfbf80da14096b976aba0687c..604f102e39fa66540c07105dd04b9dd4fe536f2f 100644 --- a/beat/editor/widgets/assetwidget.py +++ b/beat/editor/widgets/assetwidget.py @@ -29,6 +29,7 @@ from PyQt5.QtWidgets import QTabWidget from PyQt5.QtWidgets import QTextEdit from PyQt5.QtWidgets import QVBoxLayout from PyQt5.QtWidgets import QWidget +from .library import LibraryEditorWidget class AssetWidget(QWidget): @@ -56,18 +57,54 @@ class AssetWidget(QWidget): self.watcher = QFileSystemWatcher() - self.watcher.fileChanged.connect(self.show_json) + self.watcher.fileChanged.connect(self.update_editor_path) - def show_json(self, file_path): - """ Display the content of the file given in parameter + def update_editor_path(self, file_path): + """ Updates the editor to view the given JSON file - :param file_path: path to the json file to load + :param file_path: path to the json file to view & edit """ - files = self.watcher.files() if files: self.watcher.removePaths(files) self.watcher.addPath(file_path) + self.show_editor_for_path(file_path) + self.show_json(file_path) + + def show_json(self, file_path): + """ Display the content of the file given in parameter + + :param file_path: path to the json file to load + """ with open(file_path) as json_file: - self.jsonWidget.setText(json_file.read()) + contents = json_file.read() + self.jsonWidget.setText(contents) + + def show_editor_for_path(self, file_path): + """ Changes the editor tab to the correct editor + + :param file_path: path to the json file to edit + """ + + self.editor = QWidget() + if "/databases/" in file_path: + pass + elif "/dataformats/" in file_path: + pass + elif "/libraries/" in file_path: + self.editor = LibraryEditorWidget(file_path, self) + elif "/algorithms/" in file_path: + pass + elif "/toolchains/" in file_path: + pass + elif "/experiments/" in file_path: + pass + elif "/plotters/" in file_path: + pass + elif "/plotterparameters/" in file_path: + pass + + self.tabWidget.removeTab(0) + self.tabWidget.insertTab(0, self.editor, self.tr("Editor")) + self.tabWidget.setCurrentIndex(0) diff --git a/beat/editor/widgets/library.py b/beat/editor/widgets/library.py new file mode 100644 index 0000000000000000000000000000000000000000..d4dead0b87633442c69ec915e638ddc3b0e8c79e --- /dev/null +++ b/beat/editor/widgets/library.py @@ -0,0 +1,245 @@ +# vim: set fileencoding=utf-8 : +############################################################################### +# # +# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.editor module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # +# or FITNESS FOR A PARTICULAR PURPOSE. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +import re +import json +import glob +from copy import copy +from collections import OrderedDict + +from PyQt5.QtWidgets import ( + QGroupBox, + QLabel, + QLineEdit, + QComboBox, + QPushButton, + QVBoxLayout, + QGridLayout, + QSizePolicy, +) +from PyQt5.QtCore import Qt, pyqtSlot +from .editor import AbstractAssetEditor + +# use standard 12-column spacing +GRID_COLUMN_COUNT = 12 + + +def new_obj_key(keys, prefix): + key = prefix + i = 0 + while key in keys: + key = f"{prefix}{i}" + i = i + 1 + + return key + + +def deleteItemsOfLayout(layout): + if layout is not None: + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.setParent(None) + else: + deleteItemsOfLayout(item.layout()) + + +class LibraryEditorWidget(AbstractAssetEditor): + """ + This widget will show the library editor. + """ + + def __init__(self, file_path, parent=None): + """Constructor""" + + super(LibraryEditorWidget, self).__init__(parent) + with open(file_path) as json_file: + self.contents = json.loads(json_file.read(), object_pairs_hook=OrderedDict) + + # make contents['uses'] a list of 2-tuples + # this lets us manipulate it by indexes instead of just keys/values + self.contents["uses"] = list(self.contents["uses"].items()) + + re_name = re.compile(r".*/libraries/(\w+)/(\w+)/(\w+)\.json") + self.name = re.search(re_name, file_path).groups() + + # get the names of libraries besides the selected one, + # to be shown in the select menus for adding libraries + libraries_folder_path = re.search( + r"(.*/libraries)/.*.json", file_path + ).groups()[0] + libraries_files = glob.glob( + f"{libraries_folder_path}/**/*.json", recursive=True + ) + self.libraries_names = [ + "/".join(re.search(re_name, path).groups()) + for path in libraries_files + if path != file_path + ] + + self.layout = QVBoxLayout(self) + self.refresh_ui() + + def dump_json(self): + """Returns the json representation of the asset""" + # contents['uses'] is a list of 2-tuples + contents = copy(self.contents) + usesDict = OrderedDict() + for (key, val) in contents["uses"]: + usesDict[key] = val + contents["uses"] = usesDict + return json.dumps(contents) + + def refresh_ui(self): + """Re-creates the UI using the data from `self.contents`""" + deleteItemsOfLayout(self.layout) + self.init_ui() + + def init_ui(self): + layout = self.layout + + # Title + label = QLabel("/".join(map(str, self.name)), self) + label.setAlignment(Qt.AlignCenter) + + # Save Button + saveButton = QPushButton("Save Changes") + saveButton.clicked.connect(self.save_changes) + # saveButton.setAlignment(Qt.AlignCenter) + + # Description + descBox = QGroupBox("Description") + descLayout = QVBoxLayout(descBox) + descInput = QLineEdit() + descInput.setText(self.contents["description"]) + descInput.textChanged.connect(self.update_description) + descLayout.addWidget(descInput) + + # List of aliases/libraries used + usesBox = QGroupBox("Libraries used") + usesLayout = QGridLayout(usesBox) + + usesList = self.contents["uses"] + + library_options = [""] + self.libraries_names + for idx, (key, val) in enumerate(usesList): + # remove button + removeButton = QPushButton("Remove") + removeButton.clicked.connect(lambda args, idx=idx: self.remove_library(idx)) + + # the alias lineinput + aliasInput = QLineEdit() + aliasInput.setText(key) + aliasInput.textChanged.connect( + lambda alias, idx=idx: self.update_library(idx, alias, None) + ) + + # the library select box + libraryInput = QComboBox() + libraryInput.addItems(library_options) + libraryInput.setEditable(False) + libraryInput.setCurrentText(val) + libraryInput.currentTextChanged.connect( + lambda library, idx=idx: self.update_library(idx, None, library) + ) + + usesLayout.addWidget(aliasInput, idx, 0, 1, 5) + usesLayout.addWidget(libraryInput, idx, 5, 1, 5) + usesLayout.addWidget(removeButton, idx, 10, 1, 2) + + # asserts that the gridlayout logic is correct + # assert usesLayout.columnCount() == GRID_COLUMN_COUNT + # assert usesLayout.rowCount() == len(usesList) + + # button to add a new library + addButton = QPushButton( + f'Add {"A" if len(usesList) == 0 else "Another"} Library' + ) + addButton.clicked.connect(self.add_library) + usesLayout.addWidget(addButton, len(usesList), 0, 1, -1) + + # put all top-level parts together in order + layout.addWidget(label) + layout.addWidget(descBox) + layout.addWidget(usesBox) + layout.addStretch(QSizePolicy.Maximum) + layout.addWidget(saveButton) + + @pyqtSlot() + def save_changes(self): + """ + Writes the given JSON to the current library's metadata file, + using the default json library to convert. + """ + new_json = self.dump_json() + print(new_json) + + @pyqtSlot(str) + def update_description(self, text): + """ + Updates the library's description text to the given text string. + """ + # print(f'update_description {text}') + self.contents["description"] = text + + @pyqtSlot() + def add_library(self): + """ + Adds a new alias/library entry to the library. + """ + keys = [key for (key, val) in self.contents["uses"]] + key = new_obj_key(keys, "alias") + # print(f'add_library {key}') + self.contents["uses"].append((key, "")) + self.refresh_ui() + + def update_library(self, idx, alias, library): + """ + Updates the alias/library entry at the given index + to use the new alias and the new library. + + To only update one of those, just pass None to the other. + + this *doesnt* refresh the UI on purpose because the + QLineEdit & QComboBox manage their internal state + """ + # print(f'idx: {idx} alias: {alias} lib: {library}') + (old_alias, old_library) = self.contents["uses"][idx] + # avoid key collisions + if alias == old_alias: + return + alias = alias if alias is not None else old_alias + library = library if library is not None else old_library + # print(f'idx: {idx} alias: {alias} lib: {library}') + self.contents["uses"][idx] = (alias, library) + + def remove_library(self, idx): + """ + Removes the alias/library entry with the given alias. + """ + # print(f'remove_library {idx}') + del self.contents["uses"][idx] + self.refresh_ui() diff --git a/beat/editor/widgets/mainwindow.py b/beat/editor/widgets/mainwindow.py index ab024698ae7200b587d6baaa570b89d759fe5bbe..f98630f4a3dbaabde851eaebbab8bc1d53f678df 100644 --- a/beat/editor/widgets/mainwindow.py +++ b/beat/editor/widgets/mainwindow.py @@ -61,7 +61,7 @@ class MainWindow(QMainWindow): layout.addWidget(self.assetWidget) self.setCentralWidget(centralWidget) - self.assetBrowser.json_selected.connect(self.assetWidget.show_json) + self.assetBrowser.json_selected.connect(self.assetWidget.update_editor_path) settingsAction.triggered.connect(self.show_settings) self.load_settings()