#include "interface.hpp" #include "ui_interface.h" #include "savingdialog.hpp" #include "structurewriter.hpp" #include "structurereader.hpp" #include "factory.hpp" #include "transitionrule.hpp" #include "neighborhoodrule.hpp" #include "propertyvisitors.hpp" #include "modelloadingdialog.hpp" #include "configurationloadingdialog.hpp" #include "neighborhoodDialog.hpp" #include "colorlabel.h" #include #include #include #include #include #include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , simulation() , timer(new QTimer(this)) , m_customizing(false) { ui->setupUi(this); m_transition_rule = nullptr; m_neighborhood_rule = nullptr; init_transition_neighborhood_list(); update_transition_settings(); update_neighborhood_settings(); connect(ui->action_save_struct, &QAction::triggered, this, &MainWindow::afficher_interface_sauvegarde_structure); connect(ui->struct_library, &StructureLibraryView::structure_copied, this, &MainWindow::copy_structure_clicked); connect(ui->grid_view, &GridView::zoom_changed, ui->struct_library, &StructureLibraryView::update_cell_pixel_size); connect(ui->statesSettingsButton, &QPushButton::clicked, this, [this] { ColorLabel* dialog = new ColorLabel(simulation.getAlphabet(), this); if (dialog->exec()) { ui_update_alphabet(dialog->getAlphabet()); } }); connect(ui->transition_list, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { update_transition_settings(); }); connect(ui->action_bad_apple, &QAction::triggered, this, &MainWindow::play_bad_apple); connect(ui->action_propos_de_Cellulut, &QAction::triggered, this, [this](bool) { QMessageBox::about(this, "À propos de Cellulut", "Projet de C++ réalisé dans le cadre de l'UV LO21 de l'UTC.\n\n" "Contributeurs : Anthony Noir, Eugène Pin, Merwane Bouri, Arthur Detree, Yann Boucher"); }); connect(ui->action_propos_de_Qt, &QAction::triggered, this, [this](bool) { QMessageBox::aboutQt(this, "À propos de Qt"); }); connect(ui->actionCharger_depuis_une_image, &QAction::triggered, this, [this](bool) { load_from_image(); }); ui->struct_library->update_cell_pixel_size(ui->grid_view->cell_screen_size()); if (!try_load_saved_state()) { load_model(default_model()); load_grid_configuration(default_configuration()); } connect(timer, &QTimer::timeout, this, [this](){ on_nextButton_clicked(); timer->start(); }); } MainWindow::~MainWindow() { delete ui; // FIXME : il y a un risque de fuite de mémoire avec la gestion de pointeurs de m_transition_rule et m_neighborhood_rule, si l'application est quitée alors que ils pointent vers de la mémoire non transférée vers Simulation } /// Connections void MainWindow::on_simSpeedSpinbox_valueChanged(int arg1) { ui->simSpeedSlider->setValue(arg1); } void MainWindow::on_simSpeedSlider_valueChanged(int value) { ui->simSpeedSpinbox->setValue(value); } void MainWindow::on_openPatternButton_clicked() { ConfigurationLoadingDialog dialog(m_loaded_model.value("title").toString(), this); if (!dialog.exec()) return; load_grid_configuration(dialog.configuration()); } void MainWindow::on_openRuleButton_clicked() { // Indiquer qu'une nouvelle règle vient d'être ouverte. La règle est sauvegardé, donc on peut lancer la simulation ModelLoadingDialog dialog(this); if (!dialog.exec()) return; load_model(dialog.model()); } void MainWindow::on_neighborhood_list_currentTextChanged(const QString &) { update_neighborhood_settings(); } void MainWindow::on_widthSpinBox_valueChanged(int) { ui->validateGridDim->setEnabled(true); } void MainWindow::on_heightSpinBox_valueChanged(int) { ui->validateGridDim->setEnabled(true); } QString MainWindow::afficher_interface_sauvegarde_structure() { SavingDialog dialog(this); if (!dialog.exec()) return ""; QString filename = QFileDialog::getSaveFileName(this, "Choisir un nom de fichier", "", "Format Cellulut (*.json);;Format Golly RLE (*.rle)"); QFileInfo info(filename); //printf("%s\n", info.suffix().toStdString().c_str()); // on pourrait utiliser un Factory ici, mais c'est possiblement overkill, les possibilités d'évolution de format de fichier sont faibles et ne justifient pas l'effort Structure s = ui->grid_view->selected_cells(); s.author = dialog.auteur().toStdString(); s.title = dialog.titre().toStdString(); s.desc = dialog.desc().toStdString(); s.date = dialog.date().toString().toStdString(); std::string save_data; if (info.suffix() == "json") { JSONStructureWriter writer; save_data = writer.save_structure(s); } else if (info.suffix() == "rle") { RLEStructureWriter writer; save_data = writer.save_structure(s); } QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::information(this, "Impossible de créer le fichier", file.errorString()); return ""; } QTextStream stream(&file); stream << QString::fromStdString(save_data); return filename; } void MainWindow::on_validateGridDim_clicked() { Grid oldGrid = ui->grid_view->get_grid(); unsigned int nbrRow = ui->heightSpinBox->value(); // nbr de lignes => axe y unsigned int nbrCol = ui->widthSpinBox->value(); // nbr de colonne => axe x Grid newGrid(nbrRow, nbrCol); unsigned oldNbrRow = oldGrid.get_rows(); // rows = nbr de lignes => axe y unsigned oldNbrCol = oldGrid.get_col(); // col = nbr de colonne => axe x if(oldNbrRow <= nbrRow && oldNbrCol <= nbrCol) { //std::cout << "superieur\n"; fflush(stdout); for (unsigned y = 0; y < oldNbrRow ; y++) { for (unsigned x = 0; x < oldNbrCol ; x++) { Coord pos = {static_cast(x), static_cast(y)}; newGrid.set_cell(pos, oldGrid.get_state(pos)); //std::cout << "oldState : " << oldGrid.get_state(pos) << endl; } } } else { //std::cout << "superieur\n"; fflush(stdout); for (unsigned y = 0; y < nbrRow ; y++) { for (unsigned x = 0; x < nbrCol ; x++) { Coord pos = {static_cast(x), static_cast(y)}; newGrid.set_cell(pos, oldGrid.get_state(pos)); //std::cout << "oldState : " << oldGrid.get_state(pos) << endl; } } } ui->grid_view->copy_grid(newGrid); simulation.reset(); simulation.setGrid(newGrid); ui->validateGridDim->setEnabled(false); } void MainWindow::on_nbrStateComboBox_currentTextChanged(const QString &arg1) { unsigned val = arg1.toInt(); ui->grid_view->set_current_pen(val); } void MainWindow::on_randomPatternButton_clicked() { int alphabetSize = simulation.getAlphabet().taille(); Grid oldGrid = ui->grid_view->get_grid(); unsigned nbrRow = oldGrid.get_rows(); // rows = nbr de lignes => axe y unsigned nbrCol = oldGrid.get_col(); // col = nbr de colonne => axe x Grid newGrid(nbrRow, nbrCol); for (unsigned y = 0; y < nbrRow; ++y) { for (unsigned x = 0; x < nbrCol; ++x) { unsigned state = rand() % alphabetSize; Coord pos = {static_cast(x), static_cast(y)}; newGrid.set_cell(pos, state); } } ui->grid_view->copy_grid(newGrid); } void MainWindow::on_drawCellButton_clicked() { ui->grid_view->fill_selection(ui->nbrStateComboBox->currentIndex()); } void MainWindow::copy_structure_clicked(const Structure &s) { ui->grid_view->set_clipboard(s); statusBar()->showMessage("Structure copied.", 5000); } void MainWindow::init_transition_neighborhood_list() { for (const auto& transition : Factory::list_choices()) { ui->transition_list->addItem(QString::fromStdString(transition)); } for (const auto& rule : Factory::list_choices()) { ui->neighborhood_list->addItem(QString::fromStdString(rule)); } } void MainWindow::update_transition_settings() { std::string selected = ui->transition_list->currentText().toStdString(); m_transition_rule = Factory::make(selected).release(); if (!m_transition_rule) return; if (m_transition_rule->get_properties().size() == 0) ui->rule_settings_area->hide(); else { ui->rule_settings_area->show(); UIBuilderVisitor visit(ui->rule_settings_area); for (auto& prop : m_transition_rule->get_properties()) prop->accept(visit); } } void MainWindow::update_neighborhood_settings() { std::string selected = ui->neighborhood_list->currentText().toStdString(); m_neighborhood_rule = Factory::make(selected).release(); if (!m_neighborhood_rule) return; if (m_neighborhood_rule->get_properties().size() == 0) ui->neighborhood_settings_area->hide(); else { ui->neighborhood_settings_area->show(); UIBuilderVisitor visit(ui->neighborhood_settings_area); for (auto& prop : m_neighborhood_rule->get_properties()) prop->accept(visit); } } void MainWindow::enable_rule_customization() { //ui->simulation_tab->setEnabled(false); ui->savePatternButton->setEnabled(false); ui->customize_button->setText("Cancel customization"); ui->rule_settings->setEnabled(true); m_customizing = true; } void MainWindow::disable_rule_customization() { //ui->simulation_tab->setEnabled(true); ui->savePatternButton->setEnabled(true); ui->customize_button->setEnabled(true); ui->rule_settings->setEnabled(false); ui->customize_button->setText("Customize..."); m_customizing = false; } void MainWindow::load_model(const QJsonObject &obj) { ui->rule_name->setText(obj.value("title").toString()); ui->transition_list->setCurrentText(obj.value("transition_name").toString()); ui->neighborhood_list->setCurrentText(obj.value("neighborhood_name").toString()); bool alpha_initialise = false; Alphabet alpha; if (obj.value("alphabet").isArray()) { for (auto entry : obj.value("alphabet").toArray()) { if (!entry.isObject()) continue; auto entry_obj = entry.toObject(); std::string name = entry_obj.value("name").toString().toStdString(); if (!entry_obj.value("color").isArray()) continue; auto color_array = entry_obj.value("color").toArray(); stateColor color(color_array[0].toInt(), color_array[1].toInt(), color_array[2].toInt()); // Initialisation avec le premier élement if (!alpha_initialise) { alpha = Alphabet(state(color, name)); alpha_initialise = true; } else alpha.newEtat(state(color, name)); } } update_transition_settings(); update_neighborhood_settings(); disable_rule_customization(); { PropertyLoaderVisitor loader(obj.value("transition_data").toObject()); for (auto& prop : m_transition_rule->get_properties()) prop->accept(loader); UIBuilderVisitor visit(ui->rule_settings_area); for (auto& prop : m_transition_rule->get_properties()) prop->accept(visit); } { PropertyLoaderVisitor loader(obj.value("neighborhood_data").toObject()); for (auto& prop : m_neighborhood_rule->get_properties()) prop->accept(loader); UIBuilderVisitor visit(ui->neighborhood_settings_area); for (auto& prop : m_neighborhood_rule->get_properties()) prop->accept(visit); } // On transfère la propriété de ces pointeurs vers Simulation, qui en est désormais propriétaire pour l'exécution de l'automate simulation.setNeighborhoodRule(m_neighborhood_rule); simulation.setTransitionRule(m_transition_rule); simulation.reset(); ui_update_alphabet(alpha); m_loaded_model = obj; } void MainWindow::save_model() { SavingDialog dialog(this); if (!dialog.exec()) return; QString filename = QFileDialog::getSaveFileName(this, "Choisir un nom de fichier", QString(), "Modèle d'automate (*.json)"); PropertySaverVisitor trans_saver; for (auto& prop : m_transition_rule->get_properties()) prop->accept(trans_saver); PropertySaverVisitor neighborhood_saver; for (auto& prop : m_neighborhood_rule->get_properties()) prop->accept(neighborhood_saver); QJsonObject root; root["title"] = dialog.titre(); root["author"] = dialog.auteur(); root["desc"] = dialog.desc(); root["date"] = dialog.date().toString(); root["transition_name"] = ui->transition_list->currentText(); root["transition_data"] = trans_saver.save(); root["neighborhood_name"] = ui->neighborhood_list->currentText(); root["neighborhood_data"] = neighborhood_saver.save(); QJsonArray alphabet; for (unsigned i = 0; i < simulation.getAlphabet().taille(); ++i) { auto state = simulation.getAlphabet().getState(i); QJsonArray color; QJsonObject entry; entry["name"] = QString::fromStdString(state.getStateLabel()); color.push_back(state.getColor().getRed()); color.push_back(state.getColor().getGreen()); color.push_back(state.getColor().getBlue()); entry["color"] = color; alphabet.push_back(entry); } root["alphabet"] = alphabet; QJsonDocument doc(root); QFile file(filename); if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { QMessageBox::information(this, "Impossible de créer le fichier", file.errorString()); } file.write(doc.toJson()); file.close(); ui->rule_name->setText(dialog.titre()); disable_rule_customization(); load_model(root); } static unsigned closest_state(const Alphabet& alph, const QColor& color) { unsigned best_distance = std::numeric_limits::max(); unsigned best_match = 0; for (unsigned i = 0; i < alph.taille(); ++i) { state s = alph.getState(i); unsigned r_diff = qAbs(color.red() - s.getColor().getRed()); unsigned g_diff = qAbs(color.green() - s.getColor().getGreen()); unsigned b_diff = qAbs(color.blue() - s.getColor().getBlue()); unsigned distance = r_diff*r_diff + g_diff*g_diff + b_diff*b_diff; if (distance < best_distance) { best_distance = distance; best_match = i; } } return best_match; } void MainWindow::load_from_image() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),"",tr("Image (*.png *.jpg *.jpeg *.bmp *.gif)")); QImage img(fileName); if (img.isNull()) return; Grid grid = ui->grid_view->get_grid(); img = img.scaled(grid.get_col(), grid.get_rows()); for (int x = 0; x < img.width(); ++x) for (int y = 0; y < img.height(); ++y) { unsigned closest = closest_state(ui->grid_view->alphabet(), img.pixelColor(x, y)); grid.set_cell(Coord{x, y}, closest); } ui->grid_view->copy_grid(grid); } void MainWindow::ui_update_alphabet(const Alphabet &alpha) { simulation.setAlphabet(alpha); ui->struct_library->set_alphabet(alpha); ui->grid_view->set_alphabet(alpha); ui->nbrStateComboBox->clear(); for (unsigned i = 0; i < alpha.taille(); ++i) { ui->nbrStateComboBox->addItem(QString::number(i)); } } bool MainWindow::try_load_saved_state() { QFile file("saved_state.json"); if (!file.open(QFile::ReadOnly | QFile::Text)) { return false; } QTextStream in(&file); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(in.readAll().toUtf8(), &parseError); if(parseError.error != QJsonParseError::NoError) { return false; } QJsonObject data = jsonDoc.object(); QJsonObject model = data.value("model").toObject(); QJsonObject config = data.value("config").toObject(); if (model.isEmpty() || config.isEmpty()) return false; load_model(model); load_grid_configuration(config); return true; } void MainWindow::save_grid_configuration() { const Grid& grid = ui->grid_view->get_grid(); Structure grid_data = grid.to_structure(); QString title = QInputDialog::getText(this, "Choisir un titre", "Titre de la configuration : "); if (title.isEmpty()) return; RLEStructureWriter writer; std::string structure_data = writer.save_structure(grid_data); QJsonObject json_data; json_data["width"] = (int)grid.get_col(); json_data["height"] = (int)grid.get_rows(); json_data["title"] = title; json_data["model"] = m_loaded_model.value("title"); json_data["left"] = grid_data.top_left.x; json_data["top"] = grid_data.top_left.y; json_data["data"] = QString::fromStdString(structure_data); QString filename = QFileDialog::getSaveFileName(this, "Choisir un nom de fichier", "", "Configuration Cellulut (*.json)"); if (filename.isEmpty()) return; QFile file(filename); if (!file.open(QIODevice::WriteOnly | QFile::Text | QFile::Truncate)) { QMessageBox::information(this, "Impossible de créer le fichier", file.errorString()); } QJsonDocument doc(json_data); file.write(doc.toJson()); file.close(); } void MainWindow::load_grid_configuration(const QJsonObject& configuration) { Grid g(configuration.value("height").toInt(10), configuration.value("width").toInt(10)); Coord origin; origin.x = configuration.value("left").toInt(0); origin.y = configuration.value("top").toInt(0); RLEStructureReader reader(configuration.value("data").toString().toStdString()); Structure s = reader.read_structure(); ui->grid_view->copy_grid(g); ui->grid_view->paste_structure_at(origin, s); ui->widthSpinBox->setValue(g.get_col()); ui->heightSpinBox->setValue(g.get_rows()); simulation.reset(); simulation.setGrid(g); } QJsonObject MainWindow::default_model() const { const char* json = R"( { "title" : "Game of Life", "neighborhood_name" : "Moore", "neighborhood_data" : {}, "transition_name" : "Game of Life", "transition_data" : {}, "alphabet" : [ {"name" : "Dead", "color" : [255, 255, 255]}, {"name" : "Alive", "color" : [0, 0, 255]}] } )"; QJsonDocument doc = QJsonDocument::fromJson(QByteArray(json)); return doc.object(); } QJsonObject MainWindow::default_configuration() const { const char* json = R"( { "data": "x = 2, y = 2\n!", "height": 50, "left": 0, "model": "Game of Life", "title": "Game of Life Default", "top": 0, "width": 50 } )"; QJsonDocument doc = QJsonDocument::fromJson(QByteArray(json)); return doc.object(); } void MainWindow::play_bad_apple() { static QJsonArray bad_apple_frame_list; static QTimer bad_apple_timer; static bool bad_apple_connected; static QElapsedTimer bad_apple_elapsed; QFile f("extras/bad_apple.json"); if (!f.open(QFile::ReadOnly | QFile::Text)) { QMessageBox::warning(this, "", "Can't find 'extras/bad_apple.json'\n"); return; } QTextStream in(&f); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(in.readAll().toUtf8(), &parseError); QJsonObject frames = jsonDoc.object(); bad_apple_timer.stop(); bad_apple_frame_list = frames["data"].toArray(); if (!bad_apple_connected) { bad_apple_timer.callOnTimeout([this] { unsigned frame_idx = bad_apple_elapsed.elapsed()/(1000.f/30); if ((int)frame_idx >= bad_apple_frame_list.size()) { bad_apple_timer.stop(); return; } Grid grid(120, 160); QJsonObject frame = bad_apple_frame_list[frame_idx].toObject(); Coord origin; origin.x = frame.value("left").toInt(); origin.y = frame.value("top").toInt(); RLEStructureReader reader(frame.value("data").toString().toStdString()); Structure s = reader.read_structure(); s.top_left = origin; for (auto cell : s) { grid.set_cell(cell.first + s.top_left, cell.second); } ui->grid_view->copy_grid(grid); }); } //bad_apple_player.setMedia(QUrl::fromLocalFile("bad-apple.mp3")); //bad_apple_player.play(); ; bad_apple_elapsed.start(); bad_apple_timer.start(24); statusBar()->showMessage("Original : \"Bad Apple!!\" feat. Nomico by Alstroemeria Records", 60000); } void MainWindow::closeEvent(QCloseEvent *e) { // Sauvegarder l'état actuel de l'application const Grid& grid = ui->grid_view->get_grid(); Structure grid_data = grid.to_structure(); RLEStructureWriter writer; std::string structure_data = writer.save_structure(grid_data); QJsonObject config_data; config_data["width"] = (int)grid.get_col(); config_data["height"] = (int)grid.get_rows(); config_data["title"] = "Saved configuration"; config_data["model"] = m_loaded_model.value("title"); config_data["left"] = grid_data.top_left.x; config_data["top"] = grid_data.top_left.y; config_data["data"] = QString::fromStdString(structure_data); QJsonObject model = m_loaded_model; QJsonObject save_data; save_data["config"] = config_data; save_data["model"] = model; QFile file("saved_state.json"); if (!file.open(QIODevice::WriteOnly | QFile::Text | QFile::Truncate)) { return; } QJsonDocument doc(save_data); file.write(doc.toJson()); file.close(); QMainWindow::closeEvent(e); } void MainWindow::on_saveRuleButton_clicked() { save_model(); } void MainWindow::on_customize_button_clicked() { if (!m_customizing) { enable_rule_customization(); } else { disable_rule_customization(); load_model(m_loaded_model); } } void MainWindow::on_savePatternButton_clicked() { save_grid_configuration(); } void MainWindow::on_nextButton_clicked() { simulation.setGrid(ui->grid_view->get_grid()); simulation.step(); ui->grid_view->copy_grid(simulation.getGrid()); ui->stepsPeriodLabel->setText(QString::number(simulation.getPeriod())+" steps"); ui->nbStepsLabel->setText(QString::number(simulation.getTime())+" steps"); } void MainWindow::on_prevButton_clicked() { if(!simulation.back()) { QMessageBox::critical(this, "Retour arrière impossible", "L'historique est vide : impossible de revenir en arrière."); } ui->grid_view->copy_grid(simulation.getGrid()); ui->nbStepsLabel->setText(QString::number(simulation.getTime())+" steps"); } void MainWindow::on_playPauseButton_clicked() { if(timer->isActive()) { //Pause timer->stop(); } else { //Play int frequence = ui->simSpeedSpinbox->value(); if(frequence == 0) { QMessageBox::critical(this, "Impossible de démarrer", "Impossible de lancer la simulation à une vitesse nulle."); } else if(!simulation.runnable()) { QMessageBox::critical(this, "Impossible de démarrer", "Impossible de lancer la simulation : les règles ne sont pas définies ou incompatibles."); } else { timer->setInterval(1000/frequence); timer->start(); } } } void MainWindow::on_resetButton_clicked() { if(timer->isActive()) { timer->stop(); } simulation.reset(); ui->grid_view->copy_grid(simulation.getGrid()); ui->stepsPeriodLabel->setText(QString::number(simulation.getPeriod())+" steps"); ui->nbStepsLabel->setText(QString::number(simulation.getTime())+" steps"); } void MainWindow::on_recordSpinBox_valueChanged(int newSize) { simulation.setHistorySize(newSize); } void MainWindow::on_pushButton_clicked() { Grid oldGrid = ui->grid_view->get_grid(); unsigned nbrRow = oldGrid.get_rows(); // rows = nbr de lignes => axe y unsigned nbrCol = oldGrid.get_col(); // col = nbr de colonne => axe x Grid newGrid(nbrRow, nbrCol); for (unsigned y = 0; y < nbrRow; ++y) { for (unsigned x = 0; x < nbrCol; ++x) { unsigned state = 0; Coord pos = {static_cast(x), static_cast(y)}; newGrid.set_cell(pos, state); } } ui->grid_view->copy_grid(newGrid); }