From 292e26d0c8d55b1751fe2f3e69a8df39d29dcb01 Mon Sep 17 00:00:00 2001 From: Jaden Date: Wed, 20 Feb 2019 11:29:59 -0800 Subject: [PATCH 1/5] [library] Basic UX for the library editor, #178 This is a pretty simple commit showing a Qt5 interface for the library editor. Note that it's read-only right now, but the needed functions are stubbed out. --- beat/editor/widgets/assetwidget.py | 11 +- beat/editor/widgets/library.py | 165 +++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 beat/editor/widgets/library.py diff --git a/beat/editor/widgets/assetwidget.py b/beat/editor/widgets/assetwidget.py index 71b1c14..48193fe 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): @@ -70,4 +71,12 @@ class AssetWidget(QWidget): self.watcher.addPath(file_path) with open(file_path) as json_file: - self.jsonWidget.setText(json_file.read()) + contents = json_file.read() + self.jsonWidget.setText(contents) + if "/libraries/" in file_path: + self.editor = LibraryEditorWidget(file_path, contents, self) + else: + self.editor = QWidget() + 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 0000000..03c1b34 --- /dev/null +++ b/beat/editor/widgets/library.py @@ -0,0 +1,165 @@ +# 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 PyQt5.QtWidgets import ( + QWidget, + QGroupBox, + QLabel, + QLineEdit, + QComboBox, + QPushButton, + QVBoxLayout, + QGridLayout, + QSizePolicy, +) +from PyQt5.QtCore import Qt + +# use standard 12-column spacing +GRID_COLUMN_SPACING = 12 + + +class LibraryEditorWidget(QWidget): + """ + This widget will show the library editor. + """ + + def __init__(self, path, contents, parent=None): + """Constructor""" + + super(LibraryEditorWidget, self).__init__(parent) + # print(json.dumps(self.contents, indent=4)) + self.contents = json.loads(contents) + re_name = re.compile(r".*/libraries/(\w+)/(\w+)/(\w+)\.json") + self.name = re.search(re_name, 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", path).groups()[0] + libraries_files = glob.glob( + f"{libraries_folder_path}/**/*.json", recursive=True + ) + self.libraries_names = [ + "/".join(re.search(re_name, p).groups()) + for p in libraries_files + if p != path + ] + + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout(self) + self.setLayout(layout) + + # Title + label = QLabel("/".join(map(str, self.name)), self) + label.setAlignment(Qt.AlignCenter) + + # Description + descBox = QGroupBox("Description") + descLayout = QVBoxLayout(descBox) + descBox.setLayout(descLayout) + descInput = QLineEdit() + descInput.setText(self.contents["description"]) + descLayout.addWidget(descInput) + + # List of aliases/libraries used + usesBox = QGroupBox("Libraries used") + usesLayout = QGridLayout(usesBox) + usesBox.setLayout(usesLayout) + + usesDict = self.contents["uses"] + usesLayout.setVerticalSpacing(len(usesDict) + 1) + usesLayout.setHorizontalSpacing(GRID_COLUMN_SPACING) + + for idx, (key, val) in enumerate(usesDict.items()): + # print(f"key: {key} val: {json.dumps(val)}") + # Each alias/library used + + # remove button + removeButton = QPushButton("Remove") + + # the alias lineinput + aliasInput = QLineEdit() + aliasInput.setText(key) + + # the library select box + libraryInput = QComboBox() + libraryInput.addItems(self.libraries_names) + libraryInput.setEditable(False) + libraryInput.setCurrentText(val) + + usesLayout.addWidget(aliasInput, idx, 0, 1, 5) + usesLayout.addWidget(libraryInput, idx, 5, 1, 5) + usesLayout.addWidget(removeButton, idx, 10, 1, 2) + + # button to add a new library + addButton = QPushButton( + f'Use {"A" if len(usesDict) == 0 else "Another"} Library' + ) + usesLayout.addWidget(addButton, len(usesDict), 0, 1, -1) + + # put all top-level parts together in order + layout.addWidget(label) + layout.addWidget(descBox) + layout.addWidget(usesBox) + layout.addStretch(QSizePolicy.Maximum) + + def write_updated_library(new_json): + """ + Writes the given JSON to the current library's metadata file, + using the default json library to convert. + """ + pass + + def update_description(text): + """ + Updates the library's description text to the given text string. + """ + pass + + def add_library(): + """ + Adds a new alias/library entry to the library. + """ + pass + + def update_library(idx, new_alias, new_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 the old values. + """ + pass + + def remove_library(idx): + """ + Removes the alias/library entry at the given index. + """ + pass -- GitLab From 74e87ae4dbfb577832fd2e78c50d43e69e074d6f Mon Sep 17 00:00:00 2001 From: Jaden Date: Wed, 20 Feb 2019 11:33:09 -0800 Subject: [PATCH 2/5] remove redudant statements --- beat/editor/widgets/library.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beat/editor/widgets/library.py b/beat/editor/widgets/library.py index 03c1b34..f4b12a3 100644 --- a/beat/editor/widgets/library.py +++ b/beat/editor/widgets/library.py @@ -74,7 +74,6 @@ class LibraryEditorWidget(QWidget): def init_ui(self): layout = QVBoxLayout(self) - self.setLayout(layout) # Title label = QLabel("/".join(map(str, self.name)), self) @@ -83,7 +82,6 @@ class LibraryEditorWidget(QWidget): # Description descBox = QGroupBox("Description") descLayout = QVBoxLayout(descBox) - descBox.setLayout(descLayout) descInput = QLineEdit() descInput.setText(self.contents["description"]) descLayout.addWidget(descInput) @@ -91,7 +89,6 @@ class LibraryEditorWidget(QWidget): # List of aliases/libraries used usesBox = QGroupBox("Libraries used") usesLayout = QGridLayout(usesBox) - usesBox.setLayout(usesLayout) usesDict = self.contents["uses"] usesLayout.setVerticalSpacing(len(usesDict) + 1) @@ -120,7 +117,7 @@ class LibraryEditorWidget(QWidget): # button to add a new library addButton = QPushButton( - f'Use {"A" if len(usesDict) == 0 else "Another"} Library' + f'Add {"A" if len(usesDict) == 0 else "Another"} Library' ) usesLayout.addWidget(addButton, len(usesDict), 0, 1, -1) -- GitLab From 02275249086c4bac7526a2c5aae253b85e49d378 Mon Sep 17 00:00:00 2001 From: Jaden Date: Wed, 20 Feb 2019 15:57:47 -0800 Subject: [PATCH 3/5] Update library editor from feedback Derive from AbstractAssetEditor & refactor out navigation --- beat/editor/widgets/assetwidget.py | 52 +++++++++++++++++++++++------- beat/editor/widgets/library.py | 22 ++++++++----- beat/editor/widgets/mainwindow.py | 2 +- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/beat/editor/widgets/assetwidget.py b/beat/editor/widgets/assetwidget.py index 48193fe..604f102 100644 --- a/beat/editor/widgets/assetwidget.py +++ b/beat/editor/widgets/assetwidget.py @@ -57,26 +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: contents = json_file.read() self.jsonWidget.setText(contents) - if "/libraries/" in file_path: - self.editor = LibraryEditorWidget(file_path, contents, self) - else: - self.editor = QWidget() - self.tabWidget.removeTab(0) - self.tabWidget.insertTab(0, self.editor, self.tr("Editor")) - self.tabWidget.setCurrentIndex(0) + + 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 index f4b12a3..9d2c5ab 100644 --- a/beat/editor/widgets/library.py +++ b/beat/editor/widgets/library.py @@ -28,7 +28,6 @@ import json import glob from PyQt5.QtWidgets import ( - QWidget, QGroupBox, QLabel, QLineEdit, @@ -39,39 +38,46 @@ from PyQt5.QtWidgets import ( QSizePolicy, ) from PyQt5.QtCore import Qt +from .editor import AbstractAssetEditor # use standard 12-column spacing GRID_COLUMN_SPACING = 12 -class LibraryEditorWidget(QWidget): +class LibraryEditorWidget(AbstractAssetEditor): """ This widget will show the library editor. """ - def __init__(self, path, contents, parent=None): + def __init__(self, file_path, parent=None): """Constructor""" super(LibraryEditorWidget, self).__init__(parent) - # print(json.dumps(self.contents, indent=4)) - self.contents = json.loads(contents) + with open(file_path) as json_file: + self.contents = json.loads(json_file.read()) re_name = re.compile(r".*/libraries/(\w+)/(\w+)/(\w+)\.json") - self.name = re.search(re_name, path).groups() + 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", path).groups()[0] + 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, p).groups()) for p in libraries_files - if p != path + if p != file_path ] self.init_ui() + def dump_json(self): + """Returns the json representation of the asset""" + return json.dumps(self.contents) + def init_ui(self): layout = QVBoxLayout(self) diff --git a/beat/editor/widgets/mainwindow.py b/beat/editor/widgets/mainwindow.py index ab02469..f98630f 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() -- GitLab From 09a599ad842df1f543788ae2b795e4153253045e Mon Sep 17 00:00:00 2001 From: Jaden Date: Wed, 20 Feb 2019 16:12:50 -0800 Subject: [PATCH 4/5] fix the way the gridlayout is being used & var name --- beat/editor/widgets/library.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/beat/editor/widgets/library.py b/beat/editor/widgets/library.py index 9d2c5ab..2afa384 100644 --- a/beat/editor/widgets/library.py +++ b/beat/editor/widgets/library.py @@ -41,7 +41,7 @@ from PyQt5.QtCore import Qt from .editor import AbstractAssetEditor # use standard 12-column spacing -GRID_COLUMN_SPACING = 12 +GRID_COLUMN_COUNT = 12 class LibraryEditorWidget(AbstractAssetEditor): @@ -67,9 +67,9 @@ class LibraryEditorWidget(AbstractAssetEditor): f"{libraries_folder_path}/**/*.json", recursive=True ) self.libraries_names = [ - "/".join(re.search(re_name, p).groups()) - for p in libraries_files - if p != file_path + "/".join(re.search(re_name, path).groups()) + for path in libraries_files + if path != file_path ] self.init_ui() @@ -97,8 +97,6 @@ class LibraryEditorWidget(AbstractAssetEditor): usesLayout = QGridLayout(usesBox) usesDict = self.contents["uses"] - usesLayout.setVerticalSpacing(len(usesDict) + 1) - usesLayout.setHorizontalSpacing(GRID_COLUMN_SPACING) for idx, (key, val) in enumerate(usesDict.items()): # print(f"key: {key} val: {json.dumps(val)}") @@ -121,6 +119,10 @@ class LibraryEditorWidget(AbstractAssetEditor): 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(usesDict) + # button to add a new library addButton = QPushButton( f'Add {"A" if len(usesDict) == 0 else "Another"} Library' -- GitLab From 9d7deaa338616cd4a9b2fc67eb72f76a709b6d76 Mon Sep 17 00:00:00 2001 From: Jaden Date: Fri, 8 Mar 2019 11:33:46 -0800 Subject: [PATCH 5/5] Added PoC for regen UI from data, impl interactives Finally found a way to regenerate a large UI tree based on the contents of a BEAT object. Also implemented the various methods for transforming data in the library editor. --- beat/editor/widgets/library.py | 129 ++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 27 deletions(-) diff --git a/beat/editor/widgets/library.py b/beat/editor/widgets/library.py index 2afa384..d4dead0 100644 --- a/beat/editor/widgets/library.py +++ b/beat/editor/widgets/library.py @@ -26,6 +26,8 @@ import re import json import glob +from copy import copy +from collections import OrderedDict from PyQt5.QtWidgets import ( QGroupBox, @@ -37,13 +39,34 @@ from PyQt5.QtWidgets import ( QGridLayout, QSizePolicy, ) -from PyQt5.QtCore import Qt +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. @@ -54,7 +77,12 @@ class LibraryEditorWidget(AbstractAssetEditor): super(LibraryEditorWidget, self).__init__(parent) with open(file_path) as json_file: - self.contents = json.loads(json_file.read()) + 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() @@ -72,48 +100,71 @@ class LibraryEditorWidget(AbstractAssetEditor): if path != file_path ] - self.init_ui() + self.layout = QVBoxLayout(self) + self.refresh_ui() def dump_json(self): """Returns the json representation of the asset""" - return json.dumps(self.contents) + # 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 = QVBoxLayout(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) - usesDict = self.contents["uses"] - - for idx, (key, val) in enumerate(usesDict.items()): - # print(f"key: {key} val: {json.dumps(val)}") - # Each alias/library used + 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(self.libraries_names) + 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) @@ -121,50 +172,74 @@ class LibraryEditorWidget(AbstractAssetEditor): # asserts that the gridlayout logic is correct # assert usesLayout.columnCount() == GRID_COLUMN_COUNT - # assert usesLayout.rowCount() == len(usesDict) + # assert usesLayout.rowCount() == len(usesList) # button to add a new library addButton = QPushButton( - f'Add {"A" if len(usesDict) == 0 else "Another"} Library' + f'Add {"A" if len(usesList) == 0 else "Another"} Library' ) - usesLayout.addWidget(addButton, len(usesDict), 0, 1, -1) + 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) - def write_updated_library(new_json): + @pyqtSlot() + def save_changes(self): """ Writes the given JSON to the current library's metadata file, using the default json library to convert. """ - pass + new_json = self.dump_json() + print(new_json) - def update_description(text): + @pyqtSlot(str) + def update_description(self, text): """ Updates the library's description text to the given text string. """ - pass + # print(f'update_description {text}') + self.contents["description"] = text - def add_library(): + @pyqtSlot() + def add_library(self): """ Adds a new alias/library entry to the library. """ - pass + 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(idx, new_alias, new_library): + 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 the old values. - """ - pass + To only update one of those, just pass None to the other. - def remove_library(idx): + 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 at the given index. + Removes the alias/library entry with the given alias. """ - pass + # print(f'remove_library {idx}') + del self.contents["uses"][idx] + self.refresh_ui() -- GitLab