project management load/save/new (WIP)

This commit is contained in:
Anthony Rabine 2023-04-28 17:28:24 +02:00
parent 7df8e38b2c
commit 67ded5aace
39 changed files with 10239 additions and 399 deletions

245
README.md
View file

@ -1,248 +1,47 @@
# Open Story Teller (OST)
Open Story Teller is an Open Source project to provide guidelines and software to build your own story teller box.
Open Story Teller is an Open Source project to provide guidelines and software to build your own story teller box (electronics, mechanical).
The main goal is to *not* make electronics boards but instead buy or reuse existing ones. This will allow you to easily repair your device with spare parts and avoid
The main goal is to *not* make electronics boards but instead buy or reuse existing ones. This will allow you to easily repair your device with spare parts.
We propose a set of parts and firmware that is working well togather but your are free to custom everything to fit your needs.
*DO NOT CREATE YOUR OWN BOARD, JUST USE EXISTING ONES!*
This project can be used as a base platform for any device that is composed by:
- A display (TFT...)
- An Audio output
- A SD card or memory
- Some Buttons / rotary encoder / potentiometer
# Firmware/software
# Links
The firmware is highly configurable and highly portable. To achieve that, it is split in multiple parts:
- The core source file, which is common to every target
- the ports, dedicated to an embedded MCU and board
- The tests, to easily test part of source on a standard PC
- A desktop/mobile implementation
- http://openstoryteller.org/: main documentation and project news
- http://github.com/arabine/open-story-teller: source code, tickets, help
The core is written in pure C, targets implementations may add other languages and libraries (QML/C++/python ...).
# Generic player software
## GCC build system
The base software is highly portable and includes a micro virtual machine. This allow potential complex stories with loops, branches, user choices, randomization...
The project uses CMake as build system. For the embedded targets, the main CMakeLists.txt includes a generic cross compiler file that should be good for many configurations as soon as your compiler is GCC.
This project propose an minimal cross-platform GUI player application that can be used as a base project.
## Invocation
# StoryTeller Editor
1. Create a build directory (mkdir build)
2. Invoke cmake with some options, passed as definitions for CMake (-D<option>)
We propose a basic editor tool to create your own stories. The generated story script runs on our micro virtual machine and allow generate complex stories.
| Option | Role |
| -------------------- | --------------------------------------------------------- |
| TOOLCHAIN | specify the prefix name of the cross GCC binary |
| CMAKE_TOOLCHAIN_FILE | Includes before everything else a compiler toolchain file |
| CMAKE_BUILD_TYPE | Debug or Release (default ?) |
| OST_BUNDLE | Specify the bundle name to build |
| TOOLCHAIN_DIR | Specify a directory for the cross-gcc toolchain location |
![editor](images/story_editor_preview.png)
Work in progress:
- Project management
- Story pack generation
- Basic nodes (media nodes, start and stop)
Example: `cmake -DTOOLCHAIN=riscv64-unknown-elf -DCMAKE_TOOLCHAIN_FILE=cmake/cross-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=LONGAN_NANO ..`
# Hardware bundles
Here is a list of currently supported bundles. A bundle is a collection of electronics boards that are supported by official firmware builds.
Keep in mind that the official bundles proposed are not "cost-optimized". There are many rooms of improvements. The best bundles are marked with three stars "***": parts are easy to buy and the cost is minimal.
The goal of official bundles is to test the firmware on very different kind of hardware to make sure that the core firmware is highly portable.
The price indicated is purely informative.
## Raspberry Pico (Official DevKit!)
| Category | Brand | Reference | Aproximate Price |
| ------------------ | ------------------------------ | --------------------- | ---------------- |
| Main CPU board | Rasperry Pico | MKR Zero | 30€ |
| Audio | | | 15€ |
| Memory storage | SD card slot on board | | - |
| Battery management | Included LiPo charger on board | | - |
| Display | NewHaven 2.4" TFT | NHD-2.4-240320CF-BSXV | 22€ |
### Install build tools
Install build tools, example for a Debian based operating system:
- sudo apt install gcc-arm-none-eabi
- sudo apt install picolibc-arm-none-eabi
Download the pico SDK somewhere on your disk:
```
git clone https://github.com/raspberrypi/pico-sdk
```
### How to build
Copy past the following command line, execute at the directory root. Replace the PICO_SDK_PATH value with the real location on your disk where you have installed the Pico SDK.
First, create a CMake build directory:
```
mkdir build
cd build
```
Then generate the makefile (we use the Pico toolchain here, so there is no specific toolchain file to setup.)
```
cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=RASPI_PICO -DPICO_SDK_PATH=../pico-sdk -DPICO_BOARD=pico_w ..
```
This assume that the Pico SDK is located on the git project root directory. Change this path according to your real Pico SDK location.
## Sipeed Longan Nano (GD32VF103CBT6)
| Category | Maker | Name | Rounded Price |
| ------------------ | ------------------------------------ | ----------- | ------------- |
| Main CPU board | Sipeed | Longan Nano | 4€ |
| Audio | | | 15€ |
| Memory storage | Included SD card slot in Longan Nano | | - |
| Battery management | | | 15€ |
### How to build
Tools for a Debian based distro
- sudo apt install crossbuild-essential-riscv64
- sudo apt install picolibc-riscv64-unknown-elf
mkdir build
cd build
cmake -DTOOLCHAIN=riscv64-unknown-elf -DCMAKE_TOOLCHAIN_FILE=cmake/cross-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=LONGAN_NANO ..
Convert tools:
- riscv64-unknown-elf-objcopy -O binary your-file.elf your-file.hex
- riscv64-unknown-elf-objcopy -O ihex your-file.elf your-file.hex
### Wiring
TBD
## Arduino MKR Zero (SAMD21G18A)
| Category | Maker | Name | Rounded Price |
| ------------------ | ------------------------------ | --------------------- | ------------- |
| Main CPU board | Arduino | MKR Zero | 30€ |
| Audio | | | 15€ |
| Memory storage | Included SD card slot on board | | - |
| Battery management | Included LiPo charger on board | | - |
| Display | NewHaven 2.4" TFT | NHD-2.4-240320CF-BSXV | 22€ |
### How to build
Install on Ubuntu :
- sudo apt install gcc-arm-none-eabi
- sudo apt install picolibc-arm-none-eabi
cmake -DTOOLCHAIN=arm-none-eabi -DCMAKE_TOOLCHAIN_FILE=cmake/cross-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=MKR_ZERO ..
### Wiring
TBD
# Mechanical and enclosure
Use existing enclosures, build your own using wood or 3D printing... we do not propose (yet) any standard package, sorry :(
# Future targets
- ESP32 (low cost and high availability)
- RP2040
# How to build a custom Open Story Teller box
## What is a Story Teller box?
It is a little electronics device that tells stories, mainly for children. A tiny LCD screen is used only to perform choices and create some variants of the stories.
## Architecture overview
Basically, it is a portable sound device that plays mp3 and sometimes displays an image. Here is the diagram :
![proto](images/architecture.png)
## Goal
The goal of this project is to help you build your own Open Story Teller box with **existing boards**. I do not plan to build any custom board. This is because :
1. You can re-use old boards
2. You avoid wastes
3. It is relatively cheap (only if you reuse spare parts)
4. It is repairable
## Bill of materials
- A microcontroller unit board
- A rechargable battery board
- A LCD screen (320x240)
- An audio board
- Rotary switches, push buttons
- A Speaker
- A SD-Card connector
- Enclosure (wood, 3D printer-based...)
- Finally, a suitable firmware
Depending of your hardware choice, the provided firmware should be updated. The goal of this projet is to support a **selection of boards**.
# Prototype, code-name OST-1
## Presentation
For this first step, we will deliver a working prototype so that we will have a firmware to work on. This step will give us a lot of knownledge for the next step. For now, there is no price optimization.
Technology choices are :
| Part | Price | Shop |
| --- | --- | --- |
| PCM5102 Audio board | 4 € | Aliexpress |
| PAM8302 Mono Amplifier | 9 € | Adafruit |
| Longan Nano RISC-V board with SD-Card port | 4 € | Aliexpress |
| 3.2" SPI TFT Screen (320x240) with ILI9341 driver | 9 € | Aliexpress |
| Adafruit PowerBoost 500 charger | 15 € | Adafruit |
| Some Pimoroni buttons are rotary switches | 4 € | Pimoroni |
| Speaker | 4 € | Pimoroni |
| LiPo battery 500mAh | 9 € | Any |
| **TOTAL** | **58 €** |
/!\ Achtung /!\ We can easily lower some prices, especially if your MCU controller already contains a battery connector. Remember, this first step is to test various parts.
## What does it look like
The firmware is still under construction. Everything is tested on breadboard.
![proto](images/ost-1/complete.png)
What is working:
- The audio path
- The SD Card
- Roughly: playing a wav file from the SD Card
## Audio path
An I2S DAC controller with a jack output :
![proto](images/ost-1/audio_board.webp)
An audio amplifier from Adafruit (2.5W, can drive a speaker between 3 ohms and 8 ohms).
![proto](images/ost-1/audio_amplifier.png)
A speaker :
![proto](images/ost-1/speaker_4ohms_3w.png)
Planned nodes:
- Random
- Loop
- Conditional
# License
MIT License
Copyright (c) 2023 Anthony Rabine

View file

@ -23,3 +23,237 @@ features:
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
---
# Firmware/software
The firmware is highly configurable and highly portable. To achieve that, it is split in multiple parts:
- The core source file, which is common to every target
- the ports, dedicated to an embedded MCU and board
- The tests, to easily test part of source on a standard PC
- A desktop/mobile implementation
The core is written in pure C, targets implementations may add other languages and libraries (QML/C++/python ...).
## GCC build system
The project uses CMake as build system. For the embedded targets, the main CMakeLists.txt includes a generic cross compiler file that should be good for many configurations as soon as your compiler is GCC.
## Invocation
1. Create a build directory (mkdir build)
2. Invoke cmake with some options, passed as definitions for CMake (-D<option>)
| Option | Role |
| -------------------- | --------------------------------------------------------- |
| TOOLCHAIN | specify the prefix name of the cross GCC binary |
| CMAKE_TOOLCHAIN_FILE | Includes before everything else a compiler toolchain file |
| CMAKE_BUILD_TYPE | Debug or Release (default ?) |
| OST_BUNDLE | Specify the bundle name to build |
| TOOLCHAIN_DIR | Specify a directory for the cross-gcc toolchain location |
Example: `cmake -DTOOLCHAIN=riscv64-unknown-elf -DCMAKE_TOOLCHAIN_FILE=cmake/cross-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=LONGAN_NANO ..`
# Hardware bundles
Here is a list of currently supported bundles. A bundle is a collection of electronics boards that are supported by official firmware builds.
Keep in mind that the official bundles proposed are not "cost-optimized". There are many rooms of improvements. The best bundles are marked with three stars "***": parts are easy to buy and the cost is minimal.
The goal of official bundles is to test the firmware on very different kind of hardware to make sure that the core firmware is highly portable.
The price indicated is purely informative.
## Raspberry Pico (Official DevKit!)
| Category | Brand | Reference | Aproximate Price |
| ------------------ | ------------------------------ | --------------------- | ---------------- |
| Main CPU board | Rasperry Pico | MKR Zero | 30€ |
| Audio | | | 15€ |
| Memory storage | SD card slot on board | | - |
| Battery management | Included LiPo charger on board | | - |
| Display | NewHaven 2.4" TFT | NHD-2.4-240320CF-BSXV | 22€ |
### Install build tools
Install build tools, example for a Debian based operating system:
- sudo apt install gcc-arm-none-eabi
- sudo apt install picolibc-arm-none-eabi
Download the pico SDK somewhere on your disk:
```
git clone https://github.com/raspberrypi/pico-sdk
```
### How to build
Copy past the following command line, execute at the directory root. Replace the PICO_SDK_PATH value with the real location on your disk where you have installed the Pico SDK.
First, create a CMake build directory:
```
mkdir build
cd build
```
Then generate the makefile (we use the Pico toolchain here, so there is no specific toolchain file to setup.)
```
cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=RASPI_PICO -DPICO_SDK_PATH=../pico-sdk -DPICO_BOARD=pico_w ..
```
This assume that the Pico SDK is located on the git project root directory. Change this path according to your real Pico SDK location.
## Sipeed Longan Nano (GD32VF103CBT6)
| Category | Maker | Name | Rounded Price |
| ------------------ | ------------------------------------ | ----------- | ------------- |
| Main CPU board | Sipeed | Longan Nano | 4€ |
| Audio | | | 15€ |
| Memory storage | Included SD card slot in Longan Nano | | - |
| Battery management | | | 15€ |
### How to build
Tools for a Debian based distro
- sudo apt install crossbuild-essential-riscv64
- sudo apt install picolibc-riscv64-unknown-elf
mkdir build
cd build
cmake -DTOOLCHAIN=riscv64-unknown-elf -DCMAKE_TOOLCHAIN_FILE=cmake/cross-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=LONGAN_NANO ..
Convert tools:
- riscv64-unknown-elf-objcopy -O binary your-file.elf your-file.hex
- riscv64-unknown-elf-objcopy -O ihex your-file.elf your-file.hex
### Wiring
TBD
## Arduino MKR Zero (SAMD21G18A)
| Category | Maker | Name | Rounded Price |
| ------------------ | ------------------------------ | --------------------- | ------------- |
| Main CPU board | Arduino | MKR Zero | 30€ |
| Audio | | | 15€ |
| Memory storage | Included SD card slot on board | | - |
| Battery management | Included LiPo charger on board | | - |
| Display | NewHaven 2.4" TFT | NHD-2.4-240320CF-BSXV | 22€ |
### How to build
Install on Ubuntu :
- sudo apt install gcc-arm-none-eabi
- sudo apt install picolibc-arm-none-eabi
cmake -DTOOLCHAIN=arm-none-eabi -DCMAKE_TOOLCHAIN_FILE=cmake/cross-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DOST_BUNDLE=MKR_ZERO ..
### Wiring
TBD
# Mechanical and enclosure
Use existing enclosures, build your own using wood or 3D printing... we do not propose (yet) any standard package, sorry :(
# Future targets
- ESP32 (low cost and high availability)
- RP2040
# How to build a custom Open Story Teller box
## What is a Story Teller box?
It is a little electronics device that tells stories, mainly for children. A tiny LCD screen is used only to perform choices and create some variants of the stories.
## Architecture overview
Basically, it is a portable sound device that plays mp3 and sometimes displays an image. Here is the diagram :
![proto](images/architecture.png)
## Goal
The goal of this project is to help you build your own Open Story Teller box with **existing boards**. I do not plan to build any custom board. This is because :
1. You can re-use old boards
2. You avoid wastes
3. It is relatively cheap (only if you reuse spare parts)
4. It is repairable
## Bill of materials
- A microcontroller unit board
- A rechargable battery board
- A LCD screen (320x240)
- An audio board
- Rotary switches, push buttons
- A Speaker
- A SD-Card connector
- Enclosure (wood, 3D printer-based...)
- Finally, a suitable firmware
Depending of your hardware choice, the provided firmware should be updated. The goal of this projet is to support a **selection of boards**.
# Prototype, code-name OST-1
## Presentation
For this first step, we will deliver a working prototype so that we will have a firmware to work on. This step will give us a lot of knownledge for the next step. For now, there is no price optimization.
Technology choices are :
| Part | Price | Shop |
| ------------------------------------------------- | -------- | ---------- |
| PCM5102 Audio board | 4 € | Aliexpress |
| PAM8302 Mono Amplifier | 9 € | Adafruit |
| Longan Nano RISC-V board with SD-Card port | 4 € | Aliexpress |
| 3.2" SPI TFT Screen (320x240) with ILI9341 driver | 9 € | Aliexpress |
| Adafruit PowerBoost 500 charger | 15 € | Adafruit |
| Some Pimoroni buttons are rotary switches | 4 € | Pimoroni |
| Speaker | 4 € | Pimoroni |
| LiPo battery 500mAh | 9 € | Any |
| **TOTAL** | **58 €** |
/!\ Achtung /!\ We can easily lower some prices, especially if your MCU controller already contains a battery connector. Remember, this first step is to test various parts.
## What does it look like
The firmware is still under construction. Everything is tested on breadboard.
![proto](images/ost-1/complete.png)
What is working:
- The audio path
- The SD Card
- Roughly: playing a wav file from the SD Card
## Audio path
An I2S DAC controller with a jack output :
![proto](images/ost-1/audio_board.webp)
An audio amplifier from Adafruit (2.5W, can drive a speaker between 3 ohms and 8 ohms).
![proto](images/ost-1/audio_amplifier.png)
A speaker :
![proto](images/ost-1/speaker_4ohms_3w.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View file

@ -23,6 +23,7 @@ set(PROJECT_SOURCES
src/toolbar.cpp
src/ost-editor.qrc
src/story_project.cpp
src/story_project.h
src/media_node_model.h
src/media_node_model.cpp
src/story_graph_model.cpp
@ -47,6 +48,8 @@ set(PROJECT_SOURCES
src/log_dock.cpp
src/vm_dock.h
src/vm_dock.cpp
src/dock_widget_base.h
src/dock_widget_base.cpp
src/code_editor.h
src/code_editor.cpp
src/graph.h
@ -55,6 +58,9 @@ set(PROJECT_SOURCES
src/event_node_model.cpp
src/highlighter.h
src/highlighter.cpp
src/zip.cpp
src/zip.h
src/miniz.c
src/ost-hmi.ui
src/ost-vm.ui
src/ost-data.ui
@ -64,6 +70,7 @@ set(PROJECT_SOURCES
src/media-node.ui
src/event-node.ui
src/choose-file.ui
src/new-project.ui
../software/chip32/chip32_assembler.cpp
../software/chip32/chip32_vm.c
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 24 24"
version="1.1"
id="svg4"
sodipodi:docname="file-document-plus-outline.svg"
inkscape:export-filename="file-document-plus-outline.png"
inkscape:export-xdpi="599.172"
inkscape:export-ydpi="599.172"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="35.06856"
inkscape:cx="4.6908114"
inkscape:cy="10.707597"
inkscape:window-width="2112"
inkscape:window-height="1436"
inkscape:window-x="1352"
inkscape:window-y="380"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="M23 18H20V15H18V18H15V20H18V23H20V20H23M6 2C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H13.81C13.45 21.38 13.2 20.7 13.08 20H6V4H13V9H18V13.08C18.33 13.03 18.67 13 19 13C19.34 13 19.67 13.03 20 13.08V8L14 2M8 12V14H16V12M8 16V18H13V16Z"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,25 @@
#include "dock_widget_base.h"
EventFilter::EventFilter( QObject* aParent )
: QObject( aParent )
{
}
bool EventFilter::eventFilter( QObject *obj, QEvent *event )
{
if ( event->type() == QEvent::Close )
{
return true;
}
return QObject::eventFilter( obj, event );
}
DockWidgetBase::DockWidgetBase(const QString &title)
: QDockWidget(title)
{
EventFilter* filter = new EventFilter( this );
installEventFilter( filter );
}

View file

@ -0,0 +1,23 @@
#ifndef DOCK_WIDGET_BASE_H
#define DOCK_WIDGET_BASE_H
#include <QDockWidget>
#include <QEvent>
class EventFilter : public QObject
{
Q_OBJECT
public:
EventFilter( QObject* parent );
protected:
bool eventFilter(QObject *obj, QEvent *event);
};
class DockWidgetBase : public QDockWidget
{
Q_OBJECT
public:
DockWidgetBase(const QString &title);
};
#endif // DOCK_WIDGET_BASE_H

View file

@ -1,7 +1,7 @@
#include "log_dock.h"
LogDock::LogDock()
: QDockWidget(tr("Logs"))
: DockWidgetBase(tr("Logs"))
{
setObjectName("OstHmiDock"); // used to save the state
m_logUi.setupUi(this);

View file

@ -1,10 +1,10 @@
#ifndef LOGDOCK_H
#define LOGDOCK_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include "ui_ost-log.h"
class LogDock : public QDockWidget
class LogDock : public DockWidgetBase
{
public:
LogDock();

View file

@ -22,6 +22,8 @@
#include <QHeaderView>
#include <QSettings>
#include <QtDebug>
#include <QStandardPaths>
#include <QDir>
#include <QtNodes/BasicGraphicsScene>
#include <QtNodes/ConnectionStyle>
@ -49,6 +51,10 @@ MainWindow::MainWindow()
: m_model(m_project)
, m_scene(m_model)
{
SetupTemporaryProject();
RefreshProjectInformation();
Callback<void(QtMsgType , const QMessageLogContext &, const QString &)>::func = std::bind(&MainWindow::MessageOutput, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
auto cb = static_cast<message_output_t>(Callback<void(QtMsgType , const QMessageLogContext &, const QString &)>::callback);
@ -63,39 +69,36 @@ MainWindow::MainWindow()
m_logDock = new LogDock();
addDockWidget(Qt::DockWidgetArea::BottomDockWidgetArea, m_logDock);
m_toolbar->AddDockToMenu(m_logDock->toggleViewAction());
m_nodeEditorDock = new NodeEditorDock(&m_scene);
addDockWidget(Qt::DockWidgetArea::LeftDockWidgetArea, m_nodeEditorDock);
m_toolbar->addAction(m_nodeEditorDock->toggleViewAction());
m_toolbar->AddDockToMenu(m_nodeEditorDock->toggleViewAction());
m_ostHmiDock = new OstHmiDock();
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_ostHmiDock);
m_toolbar->AddDockToMenu(m_ostHmiDock->toggleViewAction());
connect(m_ostHmiDock, &OstHmiDock::sigOkButton, [=]() {
QCoreApplication::postEvent(this, new VmEvent(VmEvent::evOkButton));
});
m_resourcesDock = new ResourcesDock();
m_resourcesDock = new ResourcesDock(m_project);
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_resourcesDock);
m_toolbar->AddDockToMenu(m_resourcesDock->toggleViewAction());
m_scriptEditorDock = new ScriptEditorDock();
addDockWidget(Qt::DockWidgetArea::LeftDockWidgetArea, m_scriptEditorDock);
m_toolbar->AddDockToMenu(m_scriptEditorDock->toggleViewAction());
m_vmDock = new VmDock(m_assembler);
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_vmDock);
m_toolbar->AddDockToMenu(m_vmDock->toggleViewAction());
connect(m_vmDock, &VmDock::sigCompile, [=]() {
m_project.Clear();
// Add Resources to project
QList<Resource> res = m_resourcesDock->GetResources();
for (auto & r : res)
{
m_project.m_images.push_back(r);
}
m_resourcesDock->SaveToProject();
m_scriptEditorDock->setScript(m_project.Compile());
});
@ -109,8 +112,11 @@ MainWindow::MainWindow()
m_ramView = new MemoryViewDock("RamViewDock", "RAM");
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_ramView);
m_toolbar->AddDockToMenu(m_ramView->toggleViewAction());
m_romView = new MemoryViewDock("RomViewDock", "ROM");
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_romView);
m_toolbar->AddDockToMenu(m_romView->toggleViewAction());
m_chooseFileDialog = new QDialog(this);
m_chooseFileUi.setupUi(m_chooseFileDialog);
@ -160,9 +166,41 @@ MainWindow::MainWindow()
readSettings();
connect(m_toolbar, &ToolBar::sigNew, this, [=]() {
NewProject();
});
connect(m_toolbar, &ToolBar::sigSave, this, [=]() {
SaveProject();
});
qDebug() << "Started StoryTeller Editor";
}
void MainWindow::CloseProject()
{
}
void MainWindow::SetupTemporaryProject()
{
QString appDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
// Generate a project unique ID name
m_project.uuid = QUuid::createUuid().toString().toStdString();
m_project.working_dir = QString(appDir + QDir::separator() + m_project.uuid.c_str()).toStdString();
m_project.Initialize();
// m_resourcesDock->Initialize();
qDebug() << "Working dir is: " << m_project.working_dir.c_str();
}
void MainWindow::RefreshProjectInformation()
{
setWindowTitle(QString("StoryTeller Editor - ") + m_project.working_dir.c_str());
}
void MainWindow::MessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
m_logDock->Append(type, context, msg);
@ -189,7 +227,7 @@ void MainWindow::DisplayNode(StoryNode *m_tree, QtNodes::NodeId parentId)
if (m_tree->image >= 0)
{
std::string imageFile = m_project.m_working_dir + "/rf/" + m_project.m_images[m_tree->image].file + ".bmp";
std::string imageFile = m_project.working_dir + "/rf/" + m_project.m_images[m_tree->image].file + ".bmp";
std::cout << "Node image: " << imageFile << std::endl;
nodeInternalData["image"] = imageFile.c_str();
}
@ -391,6 +429,12 @@ void MainWindow::createStatusBar()
statusBar()->showMessage(tr("Ready"));
}
void MainWindow::NewProject()
{
}
void MainWindow::open()
{
QMimeDatabase mimeDatabase;
@ -420,32 +464,25 @@ void MainWindow::open()
QGuiApplication::restoreOverrideCursor();
}
void MainWindow::save()
void MainWindow::SaveProject()
{
QMimeDatabase mimeDatabase;
QString fileName = QFileDialog::getSaveFileName(this,
tr("Choose a file name"), ".",
mimeDatabase.mimeTypeForName("application/json").filterString());
// Open the dialog if the project file does not exists
if (!QFile::exists(m_project.file_path.c_str()))
{
// Save current project
QString fileName = QFileDialog::getSaveFileName(this, tr("Save project file"),
QDir::homePath() + "/new_story.ostproj",
tr("OpenStory Teller project (*.ostproj)"));
if (fileName.isEmpty())
return;
QFile file(fileName);
if (!file.open(QFile::WriteOnly | QFile::Text)) {
QMessageBox::warning(this, tr("Dock Widgets"),
tr("Cannot write file %1:\n%2.")
.arg(QDir::toNativeSeparators(fileName), file.errorString()));
return;
m_project.file_path = fileName.toStdString();
}
QTextStream out(&file);
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
QJsonObject saveData = m_model.save();
QJsonDocument doc(saveData);
out << doc.toJson();
qDebug() << doc.toJson();
QGuiApplication::restoreOverrideCursor();
statusBar()->showMessage(tr("Saved '%1'").arg(fileName), 2000);
statusBar()->showMessage(tr("Saved '%1'").arg(m_project.file_path.c_str()), 2000);
}
void MainWindow::about()

View file

@ -152,7 +152,7 @@ private:
// Private functions
void createActions();
void createStatusBar();
void save();
void SaveProject();
void DisplayNode(StoryNode *m_tree, QtNodes::NodeId parentId);
void about();
void open();
@ -165,6 +165,11 @@ private:
bool event(QEvent *event);
void MessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg);
void NewProject();
void SetupTemporaryProject();
void RefreshProjectInformation();
void CloseProject();
};
#endif // MAIN_WINDOW_H

View file

@ -53,17 +53,16 @@ QJsonObject MediaNodeModel::save() const
// Merge two objects
QVariantMap map = obj.toVariantMap();
map.insert(m_mediaData.toVariantMap());
map.insert(m_mediaData);
return QJsonObject::fromVariantMap(map);
}
void MediaNodeModel::load(const QJsonObject &mediaData)
{
m_mediaData = mediaData;
m_mediaData = mediaData.toVariantMap();
// Display loaded image
QString imagePath = m_mediaData["image"].toString();
if (!imagePath.isEmpty())
@ -93,6 +92,9 @@ void MediaNodeModel::setInternalData(const QVariant &value)
if (obj.contains("image")) {
setImage(obj.value("image").toString());
}
// Merge new data into local object
m_mediaData.insert(obj.toVariantMap());
}
unsigned int MediaNodeModel::nPorts(PortType portType) const

View file

@ -69,6 +69,6 @@ private:
Ui::mediaNodeUi m_ui;
std::shared_ptr<NodeData> m_nodeData;
QJsonObject m_mediaData;
QVariantMap m_mediaData;
void setImage(const QString &fileName);
};

View file

@ -2,7 +2,7 @@
#include "model/buffer/qmemorybuffer.h"
MemoryViewDock::MemoryViewDock(const QString &objectName, const QString &title)
: QDockWidget(title)
: DockWidgetBase(title)
{
setObjectName(objectName);

View file

@ -1,10 +1,10 @@
#ifndef MEMORY_VIEWDOCK_H
#define MEMORY_VIEWDOCK_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include <qhexview.h>
class MemoryViewDock : public QDockWidget
class MemoryViewDock : public DockWidgetBase
{
public:
MemoryViewDock(const QString &objectName, const QString &title);

7736
story-editor/src/miniz.c Normal file

File diff suppressed because it is too large Load diff

1350
story-editor/src/miniz.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NewProjectDialog</class>
<widget class="QDialog" name="NewProjectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>346</width>
<height>263</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>New project parameters</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Directory:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="minimumSize">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Project name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Size of display screen:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox">
<item>
<property name="text">
<string>320x240</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Image format:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_2">
<item>
<property name="text">
<string>BMP (compressed 4-bit palette)</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Sound format</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_3">
<item>
<property name="text">
<string>QOA (Quite Ok Audio)</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>18</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NewProjectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NewProjectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -2,7 +2,7 @@
#include <QOpenGLWidget>
NodeEditorDock::NodeEditorDock(BasicGraphicsScene *scene)
: QDockWidget(tr("Node editor"))
: DockWidgetBase(tr("Node editor"))
{
setObjectName("NodeEditorDock");

View file

@ -1,13 +1,14 @@
#ifndef NODEEDITORDOCK_H
#define NODEEDITORDOCK_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include <QtNodes/GraphicsView>
#include <QtNodes/BasicGraphicsScene>
using QtNodes::BasicGraphicsScene;
using QtNodes::GraphicsView;
class NodeEditorDock : public QDockWidget
class NodeEditorDock : public DockWidgetBase
{
public:
NodeEditorDock(BasicGraphicsScene *scene);

View file

@ -5,5 +5,6 @@
<file>../assets/play-circle.png</file>
<file>../assets/build.png</file>
<file>../assets/volume-off.png</file>
<file>../assets/file-document-plus-outline.svg</file>
</qresource>
</RCC>

View file

@ -3,7 +3,7 @@
#include <QPixmap>
OstHmiDock::OstHmiDock()
: QDockWidget(tr("StoryTeller HMI"))
: DockWidgetBase(tr("StoryTeller HMI"))
{
setObjectName("OstHmiDock"); // used to save the state
m_uiOstDisplay.setupUi(this);

View file

@ -1,10 +1,10 @@
#ifndef OSTHMI_DOCK_H
#define OSTHMI_DOCK_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include "ui_ost-hmi.h"
class OstHmiDock : public QDockWidget
class OstHmiDock : public DockWidgetBase
{
Q_OBJECT
public:

View file

@ -1,2 +1,47 @@
#include "resource_model.h"
QVariant ResourceModel::data(const QModelIndex &index, int role) const {
if (role != Qt::DisplayRole && role != Qt::EditRole) return {};
const auto & res = m_data[index.row()];
switch (index.column()) {
case 0: return res.file.c_str();
case 1: return res.format.c_str();
case 2: return res.description.c_str();
default: return {};
};
}
QVariant ResourceModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return {};
switch (section) {
case 0: return "File";
case 1: return "Format";
case 2: return "Description";
default: return {};
}
}
void ResourceModel::append(const Resource &res) {
beginInsertRows({}, m_data.count(), m_data.count());
m_data.append(res);
endInsertRows();
}
void ResourceModel::Clear()
{
beginResetModel();
m_data.clear();
endResetModel();
}
QString ResourceModel::GetFileName(int row) {
QString n;
if (row < m_data.size())
{
n = m_data.at(row).file.c_str();
}
return n;
}

View file

@ -1,35 +1,10 @@
#ifndef RESOURCEMODEL_H
#define RESOURCEMODEL_H
#ifndef RESOURCE_MODEL_H
#define RESOURCE_MODEL_H
#include <QtGui>
#include "story_project.h"
#include <QAbstractTableModel>
/*
class CustomTableModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit CustomTableModel(QObject *parent = nullptr);
// Reimplemented virtual functions
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
private:
const int rows = 3;
const int columns = 3;
int dataStorage[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
};
*/
class ResourceModel : public QAbstractTableModel
{
@ -39,41 +14,13 @@ public:
int rowCount(const QModelIndex &) const override { return m_data.count(); }
int columnCount(const QModelIndex &) const override { return 3; }
QVariant data(const QModelIndex &index, int role) const override {
if (role != Qt::DisplayRole && role != Qt::EditRole) return {};
const auto & res = m_data[index.row()];
switch (index.column()) {
case 0: return res.file.c_str();
case 1: return res.format.c_str();
case 2: return res.description.c_str();
default: return {};
};
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return {};
switch (section) {
case 0: return "File";
case 1: return "Format";
case 2: return "Description";
default: return {};
}
}
void append(const Resource & res) {
beginInsertRows({}, m_data.count(), m_data.count());
m_data.append(res);
endInsertRows();
}
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
void append(const Resource & res);
QString GetFileName(int row) {
QString n;
void Clear();
if (row < m_data.size())
{
n = m_data.at(row).file.c_str();
}
return n;
}
QString GetFileName(int row);
QList<Resource> GetData() const { return m_data; }
@ -81,4 +28,4 @@ private:
QList<Resource> m_data;
};
#endif // RESOURCEMODEL_H
#endif // RESOURCE_MODEL_H

View file

@ -2,8 +2,9 @@
#include <QFileDialog>
#include <QStandardPaths>
ResourcesDock::ResourcesDock()
: QDockWidget(tr("Resources"))
ResourcesDock::ResourcesDock(StoryProject &project)
: m_project(project)
, DockWidgetBase(tr("Resources"))
{
setObjectName("ResourcesDock"); // used to save the state
@ -16,13 +17,51 @@ ResourcesDock::ResourcesDock()
".",
tr("Images (*.bmp)"));
std::filesystem::path p(fileName.toStdString());
std::filesystem::path p2 = m_project.ImagesPath() / p.filename().generic_string();
std::filesystem::copy(p, p2);
Resource res;
res.format = "BMP";
res.file = fileName.toStdString();
res.file = p.filename();
m_resourcesModel.append(res);
});
connect(m_uiOstResources.addSoundButton, &QPushButton::clicked, [=](bool checked) {
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
".",
tr("Sounds (*.wav)"));
std::filesystem::path p(fileName.toStdString());
std::filesystem::path p2 = m_project.SoundsPath() / p.filename().generic_string();
std::filesystem::copy(p, p2);
Resource res;
res.format = "WAV";
res.file = p.filename();
m_resourcesModel.append(res);
});
}
void ResourcesDock::Initialize()
{
}
void ResourcesDock::SaveToProject()
{
m_project.Clear();
for (auto & r : m_resourcesModel.GetData())
{
m_project.m_images.push_back(r);
}
}
void ResourcesDock::slotClear()
{
m_resourcesModel.Clear();
}

View file

@ -1,22 +1,29 @@
#ifndef RESOURCESDOCK_H
#define RESOURCESDOCK_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include "ui_ost-resources.h"
#include "resource_model.h"
#include <filesystem>
class ResourcesDock : public QDockWidget
class ResourcesDock : public DockWidgetBase
{
Q_OBJECT
public:
ResourcesDock();
ResourcesDock(StoryProject &project);
void Initialize();
QList<Resource> GetResources() const { return m_resourcesModel.GetData(); }
ResourceModel &getModel() { return m_resourcesModel; }
void SaveToProject();
public slots:
void slotClear();
private:
// Resources
StoryProject &m_project;
Ui::ostResources m_uiOstResources;
ResourceModel m_resourcesModel;
};

View file

@ -102,7 +102,7 @@ mov R0,R2 ; copy R2 into R0 (NO blank space between , and R2)
ScriptEditorDock::ScriptEditorDock()
: QDockWidget(tr("Script editor"))
: DockWidgetBase(tr("Script editor"))
{
setObjectName("ScriptEditorDock"); // used to save the state

View file

@ -1,11 +1,11 @@
#ifndef SCRIPTEDITOR_H
#define SCRIPTEDITOR_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include "code_editor.h"
#include "highlighter.h"
class ScriptEditorDock : public QDockWidget
class ScriptEditorDock : public DockWidgetBase
{
Q_OBJECT

View file

@ -311,8 +311,11 @@ QJsonObject StoryGraphModel::saveNode(NodeId const nodeId) const
{
QJsonObject nodeJson;
nodeJson["id"] = static_cast<qint64>(nodeId);
nodeJson["internal-data"] = _models.at(nodeId)->save();
{
QPointF const pos = nodeData(nodeId, NodeRole::Position).value<QPointF>();
@ -321,8 +324,8 @@ QJsonObject StoryGraphModel::saveNode(NodeId const nodeId) const
posJson["y"] = pos.y();
nodeJson["position"] = posJson;
// nodeJson["inPortCount"] = QString::number(_nodePortCounts[nodeId].in);
// nodeJson["outPortCount"] = QString::number(_nodePortCounts[nodeId].out);
nodeJson["inPortCount"] = QString::number(nodeData(nodeId, NodeRole::InPortCount).value<int>());
nodeJson["outPortCount"] = QString::number(nodeData(nodeId, NodeRole::OutPortCount).value<int>());
}
return nodeJson;

View file

@ -3,22 +3,36 @@
#include <fstream>
#include <iostream>
#include <queue>
#include <filesystem>
#include "json.hpp"
void StoryProject::Initialize()
{
// Frist try to create the working directory
if (!std::filesystem::is_directory(working_dir))
{
std::filesystem::create_directories(working_dir);
}
m_imagesPath = std::filesystem::path(working_dir) / "images";
m_soundsPath = std::filesystem::path(working_dir) / "sounds";
std::filesystem::create_directories(m_imagesPath);
std::filesystem::create_directories(m_soundsPath);
}
bool StoryProject::Load(const std::string &file_path)
{
std::ifstream f(file_path);
bool success = false;
std::filesystem::path p(file_path);
m_working_dir= p.parent_path();
working_dir= p.parent_path();
std::cout << "Working dir is: " << m_working_dir << std::endl;
std::cout << "Working dir is: " << working_dir << std::endl;
try {
nlohmann::json j = nlohmann::json::parse(f);
m_nodes.clear();

View file

@ -3,6 +3,7 @@
#include <vector>
#include <string>
#include <filesystem>
struct StoryNode
{
@ -43,9 +44,10 @@ struct Resource
struct StoryProject
{
std::string uuid;
std::string working_dir; /// Temporary folder based on the uuid, where the archive is unzipped
std::string file_path; /// project file (archive)
std::string m_working_dir;
std::vector<StoryNode> m_nodes;
std::string m_type;
@ -57,6 +59,12 @@ struct StoryProject
StoryNode *m_tree;
// Initialize a project
// The following parameters must be set before calling this method:
// - uuid
// - working_directory
void Initialize();
bool Load(const std::string &file_path);
void CreateTree();
void Clear() {
@ -66,16 +74,20 @@ struct StoryProject
std::string Compile();
std::filesystem::path ImagesPath() const { return m_imagesPath; }
std::filesystem::path SoundsPath() const { return m_soundsPath; }
static std::string GetFileExtension(const std::string &FileName);
static std::string GetFileName(const std::string &path);
static void ReplaceCharacter(std::string &theString, const std::string &toFind, const std::string &toReplace);
public:
static void EraseString(std::string &theString, const std::string &toErase);
static std::string ToUpper(const std::string &input);
private:
std::filesystem::path m_imagesPath;
std::filesystem::path m_soundsPath;
};
#endif // STORY_PROJECT_H

View file

@ -10,65 +10,55 @@ void ToolBar::createActions(QMenuBar *menuBar)
{
QMenu *fileMenu = menuBar->addMenu(tr("&File"));
// const QIcon newIcon = QIcon::fromTheme("document-new", QIcon(":/images/new.png"));
// QAction *newLetterAct = new QAction(newIcon, tr("&New Letter"), this);
// newLetterAct->setShortcuts(QKeySequence::New);
// newLetterAct->setStatusTip(tr("Create a new form letter"));
// connect(newLetterAct, &QAction::triggered, this, &MainWindow::newLetter);
// fileMenu->addAction(newLetterAct);
// fileToolBar->addAction(newLetterAct);
// ------------ New
{
const QIcon icon = QIcon::fromTheme("document-save", QIcon(":/assets/file-document-plus-outline.svg"));
QAction *act = new QAction(icon, tr("&New project"), this);
act->setShortcuts(QKeySequence::Save);
act->setStatusTip(tr("Create a new project"));
connect(act, &QAction::triggered, this, &ToolBar::sigNew);
fileMenu->addAction(act);
addAction(act);
}
// ------------ Save
{
const QIcon saveIcon = QIcon::fromTheme("document-save", QIcon(":/assets/floppy.svg"));
QAction *saveAct = new QAction(saveIcon, tr("&Save..."), this);
saveAct->setShortcuts(QKeySequence::Save);
saveAct->setStatusTip(tr("Save the current project"));
connect(saveAct, &QAction::triggered, this, &ToolBar::sigSave);
fileMenu->addAction(saveAct);
addAction(saveAct);
const QIcon icon = QIcon::fromTheme("document-save", QIcon(":/assets/floppy.svg"));
QAction *act = new QAction(icon, tr("&Save project"), this);
act->setShortcuts(QKeySequence::Save);
act->setStatusTip(tr("Save the current project"));
connect(act, &QAction::triggered, this, &ToolBar::sigSave);
fileMenu->addAction(act);
addAction(act);
}
// ------------ Open
{
const QIcon saveIcon = QIcon::fromTheme("document-open", QIcon(":/assets/folder-open.svg"));
QAction *openAct = new QAction(saveIcon, tr("&Open..."), this);
openAct->setShortcuts(QKeySequence::Open);
openAct->setStatusTip(tr("Open the current project"));
connect(openAct, &QAction::triggered, this, &ToolBar::sigOpen);
fileMenu->addAction(openAct);
addAction(openAct);
const QIcon icon = QIcon::fromTheme("document-open", QIcon(":/assets/folder-open.svg"));
QAction *act = new QAction(icon, tr("&Open project"), this);
act->setShortcuts(QKeySequence::Open);
act->setStatusTip(tr("Open an existing project"));
connect(act, &QAction::triggered, this, &ToolBar::sigOpen);
fileMenu->addAction(act);
addAction(act);
}
/*
const QIcon printIcon = QIcon::fromTheme("document-print", QIcon(":/images/print.png"));
QAction *printAct = new QAction(printIcon, tr("&Print..."), this);
printAct->setShortcuts(QKeySequence::Print);
printAct->setStatusTip(tr("Print the current form letter"));
connect(printAct, &QAction::triggered, this, &MainWindow::print);
fileMenu->addAction(printAct);
fileToolBar->addAction(printAct);
*/
fileMenu->addSeparator();
QAction *quitAct = fileMenu->addAction(tr("&Quit"), this, &QWidget::close);
quitAct->setShortcuts(QKeySequence::Quit);
quitAct->setStatusTip(tr("Quit the application"));
// QMenu *editMenu = menuBar()->addMenu(tr("&Edit"));
// QToolBar *editToolBar = addToolBar(tr("Edit"));
// const QIcon undoIcon = QIcon::fromTheme("edit-undo", QIcon(":/images/undo.png"));
// QAction *undoAct = new QAction(undoIcon, tr("&Undo"), this);
// undoAct->setShortcuts(QKeySequence::Undo);
// undoAct->setStatusTip(tr("Undo the last editing action"));
// connect(undoAct, &QAction::triggered, this, &MainWindow::undo);
// editMenu->addAction(undoAct);
// editToolBar->addAction(undoAct);
// viewMenu = menuBar()->addMenu(tr("&View"));
menuBar->addSeparator();
m_windowsMenu = menuBar->addMenu(tr("&Windows"));
QMenu *helpMenu = menuBar->addMenu(tr("&Help"));
QAction *aboutAct = helpMenu->addAction(tr("&About"), this, &ToolBar::sigAbout);
aboutAct->setStatusTip(tr("Show the application's About box"));
}
void ToolBar::AddDockToMenu(QAction *action)
{
m_windowsMenu->addAction(action);
}

View file

@ -12,10 +12,16 @@ public:
ToolBar();
void createActions(QMenuBar *menuBar);
void AddDockToMenu(QAction *action);
signals:
void sigNew();
void sigSave();
void sigOpen();
void sigAbout();
private:
QMenu *m_windowsMenu;
};
#endif // TOOLBAR_H

View file

@ -1,7 +1,7 @@
#include "vm_dock.h"
VmDock::VmDock(Chip32::Assembler &assembler)
: QDockWidget(tr("Virtual Machine"))
: DockWidgetBase(tr("Virtual Machine"))
{
setObjectName("VirtualMachineDock"); // used to save the state

View file

@ -1,12 +1,12 @@
#ifndef VMDOCK_H
#define VMDOCK_H
#include <QDockWidget>
#include "dock_widget_base.h"
#include "ui_ost-vm.h"
#include "chip32_assembler.h"
class VmDock : public QDockWidget
class VmDock : public DockWidgetBase
{
Q_OBJECT

273
story-editor/src/zip.cpp Normal file
View file

@ -0,0 +1,273 @@
/**
* MIT License
* Copyright (c) 2019 Anthony Rabine
*/
#include "zip.h"
#include <fstream>
#include <cstring>
#include <functional>
#include <memory>
#include <filesystem>
static bool ensure_file_exists_and_is_readable(const char *pFilename)
{
FILE *p = nullptr;
#ifdef Q_OS_WIN
fopen_s(& p, pFilename, "rb");
#else
p = fopen(pFilename, "rb");
#endif
if (!p) {
return false;
}
fseek(p, 0, SEEK_END);
long int src_file_size = ftell(p);
fseek(p, 0, SEEK_SET);
if (src_file_size)
{
char buf[1];
if (fread(buf, 1, 1, p) != 1)
{
fclose(p);
return false;
}
}
fclose(p);
return true;
}
/*****************************************************************************/
Zip::Zip()
: mIsValid(false)
, mNumberOfFiles(0U)
{
std::memset(&mZipArchive, 0, sizeof(mZipArchive));
}
/*****************************************************************************/
Zip::~Zip()
{
Close();
}
/*****************************************************************************/
void Zip::CreateInMemory(const std::string &fileName)
{
mz_bool status = MZ_FALSE;
std::memset(&mZipArchive, 0, sizeof(mZipArchive));
status = mz_zip_writer_init_file(&mZipArchive, fileName.c_str(), 65537);
if (status)
{
mIsOpenForWriting = true;
mIsValid = true;
mIsOpen = true;
}
else
{
mIsOpenForWriting = false;
mIsValid = false;
}
}
/*****************************************************************************/
bool Zip::AddFile(const std::string &fileName, const std::string &archiveName)
{
mz_bool status = MZ_FALSE;
if (ensure_file_exists_and_is_readable(fileName.c_str()))
{
status = mz_zip_writer_add_file(&mZipArchive, archiveName.c_str(), fileName.c_str(), NULL, 0, MZ_NO_COMPRESSION);
}
return status == MZ_TRUE;
}
/*****************************************************************************/
void Zip::AddDirectory(const std::string &dirName)
{
std::string d = dirName + "/";
mz_zip_writer_add_mem(&mZipArchive, d.c_str(), NULL, 0, MZ_NO_COMPRESSION);
}
/*****************************************************************************/
std::vector<std::string> Zip::Unzip(std::string const &zipFile, const std::string &destination_dir, std::string const &password)
{
(void)(password);
std::vector<std::string> files = {};
mz_zip_archive zip_archive;
memset(&zip_archive, 0, sizeof(zip_archive));
auto status = mz_zip_reader_init_file(&zip_archive, zipFile.c_str(), 0);
if (!status) return files;
int fileCount = (int)mz_zip_reader_get_num_files(&zip_archive);
if (fileCount == 0)
{
mz_zip_reader_end(&zip_archive);
return files;
}
mz_zip_archive_file_stat file_stat;
if (!mz_zip_reader_file_stat(&zip_archive, 0, &file_stat))
{
mz_zip_reader_end(&zip_archive);
return files;
}
// Get root folder
// QFileInfo fileInfo(file_stat.m_filename);
// QFileInfo filePath(fileInfo.path());
// QString baseName = filePath.path();
// QString dstDir = QString::fromStdString(destination_dir);
// QString separator = "/";
std::string base = std::filesystem::path(file_stat.m_filename).parent_path().string();
// Get and print information about each file in the archive.
for (int i = 0; i < fileCount; i++)
{
if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) continue;
if (mz_zip_reader_is_file_a_directory(&zip_archive, i)) continue; // skip directories for now
std::string fileName = base + std::filesystem::path::preferred_separator + file_stat.m_filename; // make path relative
std::string destFile = destination_dir + std::filesystem::path::preferred_separator + fileName; // make full dest path
// creates the directory where the file will be decompressed
std::filesystem::create_directories(std::filesystem::path(destFile).parent_path());
// Extract file
if (mz_zip_reader_extract_to_file(&zip_archive, i, destFile.c_str(), 0))
{
files.emplace_back(destFile);
}
}
// Close the archive, freeing any resources it was using
mz_zip_reader_end(&zip_archive);
return files;
}
/*****************************************************************************/
bool Zip::Open(const std::string &zip, bool isFile)
{
mz_bool status;
mIsValid = false;
mIsOpenForWriting = true;
mNumberOfFiles = 0U;
std::memset(&mZipArchive, 0, sizeof(mZipArchive));
if (isFile)
{
// Physical file on disk
status = mz_zip_reader_init_file(&mZipArchive, zip.c_str(), 0);
}
else
{
// Zipped memory
status = mz_zip_reader_init_mem(&mZipArchive, zip.c_str(), zip.size(), 0);
}
if (status)
{
mFiles.clear();
// Get and print information about each file in the archive.
for (std::uint32_t i = 0; i < mz_zip_reader_get_num_files(&mZipArchive); i++)
{
mz_zip_archive_file_stat file_stat;
if (mz_zip_reader_file_stat(&mZipArchive, i, &file_stat))
{
mNumberOfFiles++;
mFiles.push_back(file_stat.m_filename);
//printf("Filename: \"%s\", Comment: \"%s\", Uncompressed size: %u, Compressed size: %u\n", file_stat.m_filename, file_stat.m_comment, (std::uint32_t)file_stat.m_uncomp_size, (std::uint32_t)file_stat.m_comp_size);
}
}
}
if (mNumberOfFiles == mz_zip_reader_get_num_files(&mZipArchive))
{
mIsValid = true;
mIsOpen = true;
}
return mIsValid;
}
/*****************************************************************************/
void Zip::Close()
{
if (mIsValid)
{
if (mIsOpenForWriting)
{
if (!mz_zip_writer_finalize_archive(&mZipArchive))
{
mz_zip_writer_end(&mZipArchive);
}
}
else
{
mz_zip_reader_end(&mZipArchive);
}
}
mIsOpen = false;
}
/*****************************************************************************/
struct UserData
{
char *output;
int offset;
};
static mz_bool DeflateCallback(const void *pBuf, int len, void *pUser)
{
UserData *ud = static_cast<UserData*>(pUser);
std::memcpy(ud->output + ud->offset, pBuf, len);
ud->offset += len;
(void) len;
(void) pUser;
return MZ_TRUE;
}
/*****************************************************************************/
int Zip::CompressBuffer(const char *input, size_t input_size, char *output)
{
int finalsize = -1;
tdefl_compressor Comp;
UserData ud;
ud.offset = 0U;
ud.output = output;
if (tdefl_init(&Comp, DeflateCallback, &ud, 0) == TDEFL_STATUS_OKAY)
{
if(tdefl_compress_buffer(&Comp, input, input_size, TDEFL_FINISH) == TDEFL_STATUS_DONE)
{
finalsize = ud.offset;
}
}
return finalsize;
}
/*****************************************************************************/
bool Zip::GetFile(const std::string &fileName, std::string &contents)
{
bool ret = false;
if (mIsValid)
{
size_t size;
char *p = reinterpret_cast<char *>(mz_zip_reader_extract_file_to_heap(&mZipArchive, fileName.c_str(), &size, 0));
if (p != nullptr)
{
contents.assign(p, size);
free(p);
ret = true;
}
}
return ret;
}
/*****************************************************************************/
std::vector<std::string> Zip::ListFiles()
{
return mFiles;
}
//=============================================================================
// End of file Zip.cpp
//=============================================================================

52
story-editor/src/zip.h Normal file
View file

@ -0,0 +1,52 @@
/**
* MIT License
* Copyright (c) 2019 Anthony Rabine
*/
#ifndef ZIP_H
#define ZIP_H
#include <cstdint>
#include <string>
#include <vector>
#include "miniz.h"
/*****************************************************************************/
class Zip
{
public:
Zip();
~Zip();
bool Open(const std::string &zip, bool isFile);
std::uint32_t NumberOfFiles() { return mNumberOfFiles; }
bool GetFile(const std::string &fileName, std::string &contents);
std::vector<std::string> ListFiles();
void Close();
bool IsOpen() const { return mIsOpen; }
bool isOpenForWriting() const { return mIsOpenForWriting; }
static int CompressBuffer(const char *input, size_t input_size, char *output);
void CreateInMemory(const std::string &fileName);
bool AddFile(const std::string &fileName, const std::string &archiveName);
void AddDirectory(const std::string &dirName);
static std::vector<std::string> Unzip(const std::string &zipFile, const std::string &destination_dir, const std::string &password);
private:
mz_zip_archive mZipArchive;
bool mIsValid;
bool mIsOpen{false};
bool mIsOpenForWriting{false};
std::uint32_t mNumberOfFiles;
std::vector<std::string> mFiles;
};
#endif // ZIP_H
//=============================================================================
// End of file Zip.cpp
//=============================================================================