Skip to content

Commit

Permalink
Merge pull request #37813 from robertapplin/37730-tool-to-create-mvp-…
Browse files Browse the repository at this point in the history
…template-files

Add tool for generating example MVP files from a template
  • Loading branch information
SilkeSchomann authored Sep 16, 2024
2 parents bcc838a + e0526e6 commit 1a4dba4
Show file tree
Hide file tree
Showing 14 changed files with 524 additions and 0 deletions.
68 changes: 68 additions & 0 deletions dev-docs/source/MVPDesign.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,74 @@ in C++, or `unittest.mock
can set expectations in the unit tests for certain methods to be
called, and with certain arguments.

MVP Template tool
#################

The `template.py script <https://github.com/mantidproject/mantid/blob/main/tools/MVP/template.py>`__
provides a tool which is designed to generate initial files for an MVP-based
widget written in either Python or C++. These generated files serve as a
foundation or template for creating any widget using the MVP design pattern.
It can also be used to create the necessary files when refactoring an existing
widget which is not currently using MVP. The script is designed to be run from
within a `mantid-developer` Conda environment.

Python
------

To generate files for a Python widget with name "Example", run:

.. code-block:: sh
python tools/MVP/template.py --name Example --language python --include-setup --output-dir $PWD/..
This command will generate four python files including `example_model.py`, `example_view.py`
and `example_presenter.py`. These files will be saved in the provided output directory,
as specified by `$PWD/..`. An additional file named `launch.py` will be generated if the
``--include-setup`` flag is provided to the script. This can be used to open the widget as
follows:

.. code-block:: sh
python $PWD/../launch.py
C++
---

To generate files for a C++ widget with name "Example", run:

.. code-block:: sh
python tools/MVP/template.py --name Example --language c++ --include-setup --output-dir $PWD/..
This command will generate eight files including `ExampleModel.cpp`, `ExampleModel.h`,
`ExampleView.cpp`, `ExampleView.h`, `ExamplePresenter.cpp` and `ExamplePresenter.h`.
An additional file named `main.cpp` and a `CMakeLists.txt` will be generated if the
``--include-setup`` flag is provided to the script. These files can be used to build
the widget as follows:

.. code-block:: sh
mkdir buildmvp
cd buildmvp
cmake ..
cmake --build .
The example widget can then be opened with:

.. code-block:: sh
cd buildmvp
# On a Unix system
./launch
# On a Windows system from a shell or bash
./Debug/launch.exe
The `main.cpp` and a `CMakeLists.txt` files are intended as an example for how you can
build, and then instantiate your widget. If you are refactoring or creating a new
widget for Mantid, the headers and cpp files should be included in the relevant
CMakeLists file elsewhere in the project.

Visual Design
#############

Expand Down
114 changes: 114 additions & 0 deletions tools/MVP/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright &copy; 2024 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
from os.path import abspath, dirname, join
from typing import Callable, Union

TEMPLATE_DIRECTORY = join(dirname(abspath(__file__)), "templates")


def _python_filename(file_type: str, name: Union[str, None] = None) -> str:
"""Create a filename string with a Python filename convention."""
return f"{name.lower()}_{file_type.lower()}" if name is not None else file_type.lower()


def _cpp_filename(file_type: str, name: Union[str, None] = None) -> str:
"""Create a filename string with a C++ filename convention."""
return f"{name}{file_type}" if name is not None else file_type


def _generate_setup_file(name: str, filename: Callable, setup_filename: str, extension: str, output_directory: str) -> None:
"""Generates a file which is used to setup or launch the generated MVP widget."""
template_filepath = join(TEMPLATE_DIRECTORY, f"{setup_filename}.{extension}.in")
with open(template_filepath, mode="r") as file:
content = file.read()

content = content.replace("Model", f"{name}Model")
content = content.replace("View", f"{name}View")
content = content.replace("Presenter", f"{name}Presenter")
# Only required for python launch file
content = content.replace("from model", "from " + filename("Model", name))
content = content.replace("from view", "from " + filename("View", name))
content = content.replace("from presenter", "from " + filename("Presenter", name))

output_filepath = join(output_directory, f"{setup_filename}.{extension}")
with open(output_filepath, mode="w") as file:
file.write(content)


def _generate_mvp_file(name: str, filename: Callable, file_type: str, extension: str, output_directory: str) -> None:
"""Generates a file using the corresponding template in the template directory."""
template_filepath = join(TEMPLATE_DIRECTORY, f"{filename(file_type)}.{extension}.in")
with open(template_filepath, mode="r") as file:
content = file.read()

for mvp_type in ["Model", "View", "Presenter"]:
content = content.replace(mvp_type, f"{name}{mvp_type}")

output_filepath = join(output_directory, f"{filename(file_type, name)}.{extension}")
with open(output_filepath, mode="w") as file:
file.write(content)


def _generate_python_files(name: str, include_setup: bool, output_directory: str) -> None:
"""Generate MVP files for a Python use case."""
print("Generating Python files with an MVP pattern...")

_generate_mvp_file(name, _python_filename, "View", "py", output_directory)
_generate_mvp_file(name, _python_filename, "Presenter", "py", output_directory)
_generate_mvp_file(name, _python_filename, "Model", "py", output_directory)
if include_setup:
_generate_setup_file(name, _python_filename, "launch", "py", output_directory)

print(f"Output directory: {output_directory}")
print("Done!")


def _generate_cpp_files(name: str, include_setup: bool, output_directory: str) -> None:
"""Generate MVP files for a C++ use case."""
print("Generating C++ files with an MVP pattern...")

_generate_mvp_file(name, _cpp_filename, "View", "cpp", output_directory)
_generate_mvp_file(name, _cpp_filename, "Presenter", "cpp", output_directory)
_generate_mvp_file(name, _cpp_filename, "Model", "cpp", output_directory)
_generate_mvp_file(name, _cpp_filename, "View", "h", output_directory)
_generate_mvp_file(name, _cpp_filename, "Presenter", "h", output_directory)
_generate_mvp_file(name, _cpp_filename, "Model", "h", output_directory)
if include_setup:
_generate_setup_file(name, _cpp_filename, "main", "cpp", output_directory)
_generate_setup_file(name, _cpp_filename, "CMakeLists", "txt", output_directory)

print(f"Output directory: {output_directory}")
print("Done!")


def _generate_files(name: str, language: str, include_setup: bool, output_directory: str) -> None:
"""Generate MVP files for a specific programming language."""
match language.lower():
case "python":
_generate_python_files(name, include_setup, output_directory)
case "c++":
_generate_cpp_files(name, include_setup, output_directory)
case _:
raise ValueError(f"An unsupported language '{language}' has been provided. Choose one: [Python, C++].")


if __name__ == "__main__":
from argparse import ArgumentParser, BooleanOptionalAction

parser = ArgumentParser(description="Generates files which can be used as an initial Model-View-Presenter template.")
parser.add_argument("-n", "--name", required=True, help="The base name to use for the files and classes.")
parser.add_argument("-l", "--language", required=True, help="The language to generate template MVP files for [Python or C++].")
parser.add_argument(
"-s",
"--include-setup",
action=BooleanOptionalAction,
help="Whether to include setup files such as a launch script (and CMakeLists.txt for C++).",
)
parser.add_argument("-o", "--output-dir", required=True, help="The absolute path to output the generated files to.")
args = parser.parse_args()

_generate_files(args.name.capitalize(), args.language, args.include_setup, args.output_dir)
29 changes: 29 additions & 0 deletions tools/MVP/templates/CMakeLists.txt.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.21)

project(MVPWidget)

set(CMAKE_CXX_STANDARD 17)

find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)

qt5_wrap_cpp(MOC_FILES View.h)

add_executable(launch
main.cpp
Model.cpp
View.cpp
Presenter.cpp
${MOC_FILES}
)

# Link with the Release version of the standard library. This is required because
# Conda does not provide packages with Debug symbols.
if(WIN32)
set_property(TARGET launch PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
endif()

target_link_libraries(launch
Qt5::Core
Qt5::Gui
Qt5::Widgets
)
18 changes: 18 additions & 0 deletions tools/MVP/templates/Model.cpp.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright &copy; 2024 ISIS Rutherford Appleton Laboratory UKRI,
// NScD Oak Ridge National Laboratory, European Spallation Source,
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
// SPDX - License - Identifier: GPL - 3.0 +
#include "Model.h"


Model::Model() : m_count(0u) {}

std::size_t Model::count() const {
return m_count;
}

void Model::setCount(std::size_t const value) {
m_count = value;
}
34 changes: 34 additions & 0 deletions tools/MVP/templates/Model.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright &copy; 2024 ISIS Rutherford Appleton Laboratory UKRI,
// NScD Oak Ridge National Laboratory, European Spallation Source,
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
// SPDX - License - Identifier: GPL - 3.0 +
#pragma once

#include <string>
#include <cstddef>


class IModel {
public:
virtual ~IModel() = default;

virtual std::size_t count() const = 0;

virtual void setCount(std::size_t const value) = 0;
};

class Model final : public IModel {

public:
Model();
~Model() override = default;

std::size_t count() const override;

void setCount(std::size_t const value) override;

private:
std::size_t m_count;
};
22 changes: 22 additions & 0 deletions tools/MVP/templates/Presenter.cpp.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright &copy; 2024 ISIS Rutherford Appleton Laboratory UKRI,
// NScD Oak Ridge National Laboratory, European Spallation Source,
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
// SPDX - License - Identifier: GPL - 3.0 +
#include "Presenter.h"

#include "Model.h"
#include "View.h"


Presenter::Presenter(std::unique_ptr<IModel> model, IView *view) : m_model(std::move(model)), m_view(view) {
// Use a subscriber to avoid Qt connections in the presenter
m_view->subscribe(this);
}

void Presenter::handleButtonClicked() {
// An example method to handle a view event
m_model->setCount(m_model->count() + 1u);
m_view->setLabel(std::to_string(m_model->count()));
}
35 changes: 35 additions & 0 deletions tools/MVP/templates/Presenter.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright &copy; 2024 ISIS Rutherford Appleton Laboratory UKRI,
// NScD Oak Ridge National Laboratory, European Spallation Source,
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
// SPDX - License - Identifier: GPL - 3.0 +
#pragma once

#include "Model.h"

#include <string>
#include <memory>


class IView;

class IPresenter {
public:
virtual ~IPresenter() = default;

virtual void handleButtonClicked() = 0;
};

class Presenter final : public IPresenter {

public:
Presenter(std::unique_ptr<IModel> model, IView *view);
~Presenter() override = default;

void handleButtonClicked() override;

private:
std::unique_ptr<IModel> m_model;
IView *m_view;
};
43 changes: 43 additions & 0 deletions tools/MVP/templates/View.cpp.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright &copy; 2024 ISIS Rutherford Appleton Laboratory UKRI,
// NScD Oak Ridge National Laboratory, European Spallation Source,
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
// SPDX - License - Identifier: GPL - 3.0 +
#include "View.h"

#include "Presenter.h"

#include <string>

#include <QVBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QString>


View::View(QWidget *parent) : QWidget(parent), m_presenter() {
auto layout = new QVBoxLayout();
auto button = new QPushButton("Increment", this);
m_label = new QLabel("0", this);

layout->addWidget(button);
layout->addWidget(m_label);

setLayout(layout);

connect(button, &QPushButton::clicked, this, &View::notifyButtonClicked);
}

void View::subscribe(IPresenter *presenter) {
m_presenter = presenter;
}

void View::notifyButtonClicked() {
// An example event slot which notifies the presenter
m_presenter->handleButtonClicked();
}

void View::setLabel(std::string const &text) {
m_label->setText(QString::fromStdString(text));
}
Loading

0 comments on commit 1a4dba4

Please sign in to comment.