#include "mainwindow.h" #include "gschighlighter.h" #include "ui_mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , mFolderPath() , mOpenGscPath() , mOpenTxtPath() , mNewGscPath() , mNewTxtPath() , mRecentFiles() , mPinnedFiles() , mRecentFolders() , mPinnedFolders() , mPinIcon(":/images/pin") , mFolderRootItem() , mFileRootItem() , mHighlighter() , mCompleter() { ui->setupUi(this); ui->tabWidget->setContextMenuPolicy(Qt::CustomContextMenu); ui->treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); mFileRootItem = new QTreeWidgetItem(ui->treeWidget); mFileRootItem->setText(0, "Files"); mFolderRootItem = new QTreeWidgetItem(ui->treeWidget); mFolderRootItem->setText(0, "Folders"); mHighlighter = new GSCHighlighter(); mCompleter = new QCompleter(this); mCompleter->setModel(modelFromFile(":/wordlist")); mCompleter->setModelSorting(QCompleter::CaseInsensitivelySortedModel); mCompleter->setCaseSensitivity(Qt::CaseInsensitive); mCompleter->setWrapAround(false); // Connect menu actions to tab widget connect(ui->actionClose, &QAction::triggered, ui->tabWidget, &TabWidget::closeCurrentTab); connect(ui->actionClose_All, &QAction::triggered, ui->tabWidget, &TabWidget::closeAllTabs); connect(ui->actionSave, &QAction::triggered, ui->tabWidget, &TabWidget::saveCurrentTab); connect(ui->actionSave_All, &QAction::triggered, ui->tabWidget, &TabWidget::saveAllTabs); connect(ui->actionSave_As, &QAction::triggered, ui->tabWidget, &TabWidget::saveCurrentTabAs); connect(ui->tabWidget, &QTabWidget::currentChanged, this, [this](int aTabIndex) { setWindowTitle(QString("Sanguine Studio - %1").arg(ui->tabWidget->tabPath(aTabIndex))); GSCEditor* editor = dynamic_cast(ui->tabWidget->widget(aTabIndex)); if (!editor) { return; } editor->setCompleter(mCompleter); mHighlighter->setDocument(editor->document()); }); connect(ui->tabWidget, &TabWidget::tabClosed, this, [this](const QString& aTabPath) { for (int i = 0; i < mFileRootItem->childCount(); i++) { QTreeWidgetItem* currentMatch = mFileRootItem->child(i); const QString matchText = currentMatch->text(1); if (aTabPath == matchText) { delete currentMatch; } } }); QShortcut* shortcut = new QShortcut(QKeySequence(tr("Ctrl+S")), this); connect(shortcut, &QShortcut::activated, this, [this]() { int tabIndex = ui->tabWidget->currentIndex(); if (tabIndex == -1) { return; } QString tabText = ui->tabWidget->tabText(tabIndex); if (!tabText.contains("*")) { return; } GSCEditor* editor = dynamic_cast(ui->tabWidget->widget(tabIndex)); if (!editor) { return; } const QString filePath = editor->property("Path").toString(); saveFile(editor->toPlainText().toUtf8(), filePath); ui->tabWidget->setTabText(tabIndex, tabText.replace("*", "")); }); QSettings settings; if (!settings.contains("folder_path")) { mFolderPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { mFolderPath = settings.value("folder_path").toString(); } if (!settings.contains("open_gsc_path")) { mOpenGscPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { mOpenGscPath = settings.value("open_gsc_path").toString(); } if (!settings.contains("open_txt_path")) { mOpenTxtPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { mOpenTxtPath = settings.value("open_txt_path").toString(); } if (!settings.contains("new_gsc_path")) { mNewGscPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { mNewGscPath = settings.value("new_gsc_path").toString(); } if (!settings.contains("new_txt_path")) { mNewTxtPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } else { mNewTxtPath = settings.value("new_txt_path").toString(); } if (!settings.contains("recent_files")) { mRecentFiles = QStringList(); } else { mRecentFiles = settings.value("recent_files").toStringList(); } updateRecentFiles(); if (!settings.contains("recent_folders")) { mRecentFolders = QStringList(); } else { mRecentFolders = settings.value("recent_folders").toStringList(); } updateRecentFolders(); if (!settings.contains("pinned_files")) { mPinnedFiles = QStringList(); } else { mPinnedFiles = settings.value("pinned_files").toStringList(); } openPinnedFiles(); if (!settings.contains("pinned_folders")) { mPinnedFolders = QStringList(); } else { mPinnedFolders = settings.value("pinned_folders").toStringList(); } openPinnedFolders(); } MainWindow::~MainWindow() { QSettings settings; settings.setValue("folder_path", mFolderPath); settings.setValue("open_gsc_path", mOpenGscPath); settings.setValue("open_txt_path", mOpenTxtPath); settings.setValue("new_gsc_path", mNewGscPath); settings.setValue("new_txt_path", mNewTxtPath); settings.setValue("recent_files", mRecentFiles); settings.setValue("recent_folders", mRecentFolders); settings.setValue("pinned_files", mPinnedFiles); settings.setValue("pinned_folders", mPinnedFolders); delete ui; delete mFolderRootItem; delete mFileRootItem; delete mHighlighter; delete mCompleter; } QAbstractItemModel* MainWindow::modelFromFile(const QString& fileName) { QFile file(fileName); if (!file.open(QFile::ReadOnly)) return new QStringListModel(mCompleter); QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); QStringList words; while (!file.atEnd()) { QByteArray line = file.readLine(); if (!line.isEmpty()) words << QString::fromUtf8(line.trimmed()); } QGuiApplication::restoreOverrideCursor(); return new QStringListModel(words, mCompleter); } void MainWindow::openFile(const QString &aFilePath) { // Write file if it doesn't already exist QFile gscFile(aFilePath); if (!gscFile.open(QIODevice::ReadWrite)) { qDebug() << "Failed to open" << aFilePath; return; } const QString fileStem = aFilePath.split('/').last(); // If has children, check if they exist in the tree already if (mFileRootItem->childCount()) { for (int i = 0; i < mFileRootItem->childCount(); i++) { // Get child and check validity QTreeWidgetItem* childItem = mFileRootItem->child(i); if (!childItem) { continue; } // Check if path contains previous gsc if (fileStem == childItem->text(0)) { qDebug() << fileStem << "already exists!"; return; } } } addRecentFile(aFilePath); setWindowTitle(QString("Sanguine Studio - %1").arg(aFilePath)); QTreeWidgetItem *openFileItem = new QTreeWidgetItem(mFileRootItem); openFileItem->setText(0, fileStem); openFileItem->setText(1, aFilePath); GSCEditor* newEditor = createEditor(fileStem, aFilePath); newEditor->setSaveableText(QString::fromUtf8(gscFile.readAll())); connect(newEditor, &GSCEditor::saveStatusChanged, ui->tabWidget, &TabWidget::refreshCurrentTab); ui->treeWidget->expandItem(mFileRootItem); } void MainWindow::pinFile(const QString &aFilePath) { if (mPinnedFiles.contains(aFilePath)) { return; } mPinnedFiles.push_back(aFilePath); updatePinnedFiles(); } void MainWindow::updatePinnedFiles() { for (int i = 0; i < mPinnedFiles.size(); i++) { int tabIndex = ui->tabWidget->tabIndexFromPath(mPinnedFiles[i]); if (tabIndex != -1) { ui->tabWidget->setTabIcon(tabIndex, mPinIcon); } QList treeItems = ui->treeWidget->findItems(mPinnedFiles[i], Qt::MatchExactly, 1); for (int i = 0; i < treeItems.size(); i++) { QTreeWidgetItem* currentItem = treeItems[i]; currentItem->setIcon(0, mPinIcon); } } } void MainWindow::openPinnedFiles() { for (int i = 0; i < mPinnedFiles.size(); i++) { openFile(mPinnedFiles[i]); } updatePinnedFiles(); } void MainWindow::openSubFolder(const QString &aFolderPath, QTreeWidget *aTreeWidget, QTreeWidgetItem *aItem, QTreeWidgetItem* aRoot) { if (!aItem) { return; } QDir dir(aFolderPath); if (!dir.exists()) { return; } dir.setFilter(QDir::Dirs | QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); int size = list.size(); for (int i = 0; i < size; i++) { QFileInfo info = list.at(i); if (info.fileName() == "." || info.fileName() == "..") continue; if (info.isDir()) { QTreeWidgetItem *fileItem = new QTreeWidgetItem(QStringList() << info.fileName(), 0); //0 means directory fileItem->setText(1, info.filePath()); if (aTreeWidget == nullptr) { aItem->addChild(fileItem); } else { aRoot->addChild(fileItem); } openSubFolder(info.filePath(), nullptr, fileItem, aRoot); } else { QTreeWidgetItem *fileItem = new QTreeWidgetItem(QStringList() << info.fileName(), 1); //1 means it is a file fileItem->setText(1, info.filePath()); if (aTreeWidget == nullptr) { aItem->addChild(fileItem); } else { aRoot->addChild(fileItem); } } } } void MainWindow::pinFolder(const QString &aFolderPath) { if (mPinnedFolders.contains(aFolderPath)) { return; } mPinnedFolders.push_back(aFolderPath); updatePinnedFolders(); } void MainWindow::updatePinnedFolders() { for (int i = 0; i < mPinnedFolders.size(); i++) { } } void MainWindow::openPinnedFolders() { for (int i = 0; i < mPinnedFolders.size(); i++) { openFolder(mPinnedFolders[i]); } } void MainWindow::addRecentFolder(const QString &aFolderPath) { // if in list, move to top if (mRecentFolders.contains(aFolderPath)) { mRecentFolders.removeOne(aFolderPath); } mRecentFolders.push_front(aFolderPath); if (mRecentFolders.size() > MAX_RECENT_FOLDERS) { mRecentFolders.removeLast(); } updateRecentFolders(); } void MainWindow::updateRecentFolders() { if (mRecentFolders.isEmpty()) { ui->actionRecent_Folders->setEnabled(false); return; } ui->actionRecent_Folders->setEnabled(true); QMenu* menu = ui->actionRecent_Folders->menu(); if (!menu) { menu = new QMenu(); ui->actionRecent_Folders->setMenu(menu); } else { menu->clear(); } for (int i = 0; i < mRecentFolders.size(); i++) { const QString recentFolderPath = mRecentFolders[i]; QAction* recentFolderAction = new QAction(menu); recentFolderAction->setText(QString("%1. %2").arg(i + 1).arg(recentFolderPath)); menu->addAction(recentFolderAction); connect(recentFolderAction, &QAction::triggered, this, [recentFolderPath, this]() { openFolder(recentFolderPath); }); } } void MainWindow::addRecentFile(const QString &aFilePath) { // if in list, move to top if (mRecentFiles.contains(aFilePath)) { mRecentFiles.removeOne(aFilePath); } mRecentFiles.push_front(aFilePath); if (mRecentFiles.size() > MAX_RECENT_FILES) { mRecentFiles.removeLast(); } updateRecentFiles(); } void MainWindow::updateRecentFiles() { if (mRecentFiles.isEmpty()) { ui->actionRecent_Files->setEnabled(false); return; } ui->actionRecent_Files->setEnabled(true); QMenu* menu = ui->actionRecent_Files->menu(); if (!menu) { menu = new QMenu(); ui->actionRecent_Files->setMenu(menu); } else { menu->clear(); } for (int i = 0; i < mRecentFiles.size(); i++) { const QString recentFilePath = mRecentFiles[i]; QAction* recentFileAction = new QAction(menu); recentFileAction->setText(QString("%1. %2").arg(i + 1).arg(recentFilePath)); menu->addAction(recentFileAction); connect(recentFileAction, &QAction::triggered, this, [recentFilePath, this]() { openFile(recentFilePath); }); } } void MainWindow::openFolder(const QString &aFolderPath) { QTreeWidgetItem* newFolderItem = new QTreeWidgetItem(mFolderRootItem); newFolderItem->setText(0, mFolderPath.split('/').last()); addRecentFolder(mFolderPath); openSubFolder(mFolderPath, ui->treeWidget, newFolderItem, newFolderItem); ui->treeWidget->expandItem(mFolderRootItem); } void MainWindow::on_actionOpen_Folder_triggered() { QFileDialog fileDialog(this); fileDialog.setFileMode(QFileDialog::Directory); fileDialog.setWindowTitle("Open Folder"); fileDialog.setDirectory(mFolderPath); fileDialog.setOptions(QFileDialog::ShowDirsOnly); if (fileDialog.exec() == QDialog::Rejected) { return; } mFolderPath = fileDialog.directory().path(); openFolder(mFolderPath); ui->treeWidget->expandItem(mFolderRootItem); } void MainWindow::on_actionOpenGSC_File_gsc_triggered() { QFileDialog fileDialog(this); fileDialog.setFileMode(QFileDialog::ExistingFile); fileDialog.setWindowTitle("Open GSC File"); fileDialog.setNameFilter("Script Files (*.gsc *.csc *.gsh);;All Files (*.*)"); fileDialog.setDirectory(mOpenGscPath); if (fileDialog.exec() == QDialog::Rejected) { return; } const QStringList selectedFiles = fileDialog.selectedFiles(); mOpenGscPath = selectedFiles.first(); openFile(mOpenGscPath); } void MainWindow::on_actionOpenText_File_txt_triggered() { QFileDialog fileDialog(this); fileDialog.setFileMode(QFileDialog::ExistingFile); fileDialog.setWindowTitle("Open TXT File"); fileDialog.setNameFilter("TXT Files (*.txt);;All Files (*.*)"); fileDialog.setDirectory(mOpenTxtPath); if (fileDialog.exec() == QDialog::Rejected) { return; } const QStringList selectedFiles = fileDialog.selectedFiles(); mOpenTxtPath = selectedFiles.first(); openFile(mOpenTxtPath); } void MainWindow::on_actionNewGSC_File_gsc_triggered() { QFileDialog fileDialog(this); fileDialog.setFileMode(QFileDialog::AnyFile); fileDialog.setWindowTitle("New GSC File"); fileDialog.setNameFilter("Script Files (*.gsc *.csc *.gsh);;All Files (*.*)"); fileDialog.setDirectory(mNewGscPath); if (fileDialog.exec() == QDialog::Rejected) { return; } const QStringList selectedFiles = fileDialog.selectedFiles(); mNewGscPath = selectedFiles.first(); openFile(mNewGscPath); } void MainWindow::on_actionNewText_File_txt_triggered() { QFileDialog fileDialog(this); fileDialog.setFileMode(QFileDialog::AnyFile); fileDialog.setWindowTitle("New TXT File"); fileDialog.setNameFilter("TXT Files (*.txt);;All Files (*.*)"); fileDialog.setDirectory(mNewTxtPath); if (fileDialog.exec() == QDialog::Rejected) { return; } const QStringList selectedFiles = fileDialog.selectedFiles(); mNewTxtPath = selectedFiles.first(); openFile(mNewTxtPath); } void MainWindow::on_actionConsole_triggered() { } void MainWindow::on_actionDeveloperMode_triggered() { DeveloperDialog devDialog; devDialog.exec(); } void MainWindow::on_actionFive_State_Modding_Website_triggered() { QDesktopServices::openUrl(QUrl("https://fivestarmodding.xyz/")); } void MainWindow::on_action5SM_Discord_triggered() { QDesktopServices::openUrl(QUrl("https://discord.gg/xSfEuQ67jV")); } void MainWindow::on_actionAbout_Sanguine_triggered() { AboutDialog aboutDialog; aboutDialog.exec(); } void MainWindow::on_actionRedLine_Solutions_Website_triggered() { QDesktopServices::openUrl(QUrl("https://redline.llc/")); } void MainWindow::on_treeWidget_itemSelectionChanged() { QTreeWidgetItem* selectedItem = ui->treeWidget->selectedItems().first(); const QString selectedText = selectedItem->text(0); const QString selectedPath = selectedItem->text(1); const QString fileStem = selectedPath.split('/').last(); if (!selectedText.contains(".gsc") && !selectedText.contains(".csc") && !selectedText.contains(".txt")) { return; } for (int i = 0; i < ui->tabWidget->count(); i++) { if (ui->tabWidget->tabText(i).replace("*", "") == selectedText) { ui->tabWidget->setCurrentIndex(i); return; } } // Write file if it doesn't already exist QFile gscFile(selectedPath); if (!gscFile.open(QIODevice::ReadWrite)) { qDebug() << "Failed to open" << selectedPath; return; } GSCEditor* newEditor = createEditor(selectedText, selectedPath); newEditor->setSaveableText(QString::fromUtf8(gscFile.readAll())); newEditor->setCompleter(mCompleter); mHighlighter->setDocument(newEditor->document()); connect(newEditor, &GSCEditor::saveStatusChanged, ui->tabWidget, &TabWidget::refreshCurrentTab); } GSCEditor* MainWindow::createEditor(QString aTabName, QString aPath) { GSCEditor* newEditor = new GSCEditor(); newEditor->setProperty("Path", QVariant::fromValue(aPath)); newEditor->setCompleter(mCompleter); mHighlighter->setDocument(newEditor->document()); int tabIndex = ui->tabWidget->addTab(newEditor, aTabName); ui->tabWidget->setCurrentIndex(tabIndex); return newEditor; } void MainWindow::saveFile(const QByteArray& aData, const QString aFilePath) { QFile saveFile(aFilePath); if (!saveFile.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { qDebug() << "ERROR: Failed to open file " << aFilePath; return; } saveFile.write(aData); } void MainWindow::on_tabWidget_customContextMenuRequested(const QPoint &pos) { int tabIndex = ui->tabWidget->tabBar()->tabAt(pos); if (tabIndex == -1) { return; } QAction *closeFileAction = new QAction(this); closeFileAction->setText("Close File"); connect(closeFileAction, &QAction::triggered, this, [this, tabIndex]() { ui->tabWidget->closeTab(tabIndex); }); QAction *saveFileAction = new QAction(this); saveFileAction->setText("Save File"); connect(saveFileAction, &QAction::triggered, this, [this, tabIndex]() { ui->tabWidget->saveTab(tabIndex); }); QAction *saveFileAsAction = new QAction(this); saveFileAsAction->setText("Save Files As"); connect(saveFileAsAction, &QAction::triggered, this, [this, tabIndex]() { ui->tabWidget->saveTabAs(tabIndex); }); QAction *pinFileAction = new QAction(this); pinFileAction->setText("Pin File"); connect(pinFileAction, &QAction::triggered, this, [this, tabIndex]() { pinFile(ui->tabWidget->tabPath(tabIndex)); }); if (!ui->tabWidget->tabText(tabIndex).contains("*")) { saveFileAction->setEnabled(false); } QMenu *tabWidgetContextMenu = new QMenu; tabWidgetContextMenu->addAction(closeFileAction); tabWidgetContextMenu->addSeparator(); tabWidgetContextMenu->addAction(saveFileAction); tabWidgetContextMenu->addAction(saveFileAsAction); tabWidgetContextMenu->addSeparator(); tabWidgetContextMenu->addAction(pinFileAction); tabWidgetContextMenu->exec(QCursor::pos()); } void MainWindow::on_actionRemove_Comments_triggered() { } void MainWindow::on_actionBeautify_Code_triggered() { } void MainWindow::on_actionClear_Saved_Data_triggered() { mFolderPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); mOpenGscPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); mOpenTxtPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); mNewGscPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); mNewTxtPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); mRecentFiles = QStringList(); mRecentFolders = QStringList(); ui->tabWidget->closeAllTabs(); QApplication::quit(); } void MainWindow::on_treeWidget_customContextMenuRequested(const QPoint &pos) { QTreeWidgetItem* item = ui->treeWidget->itemAt(pos); if (!item) { return; } const QString selectedText = item->text(0); const QString selectedPath = item->text(1); const QString fileStem = selectedPath.split('/').last(); if (!selectedText.contains(".gsc") && !selectedText.contains(".csc") && !selectedText.contains(".txt")) { return; } int tabIndex = -1; for (int i = 0; i < ui->tabWidget->count(); i++) { if (ui->tabWidget->tabPath(i) == selectedPath) { tabIndex = i; break; } } QAction *closeFileAction = new QAction(this); closeFileAction->setText("Close File"); connect(closeFileAction, &QAction::triggered, this, [this, tabIndex, &item]() { if (ui->tabWidget->closeTab(tabIndex)) { delete item; } }); QAction *saveFileAction = new QAction(this); saveFileAction->setText("Save File"); connect(saveFileAction, &QAction::triggered, this, [this, tabIndex]() { ui->tabWidget->saveTab(tabIndex); }); QAction *saveFileAsAction = new QAction(this); saveFileAsAction->setText("Save Files As"); connect(saveFileAsAction, &QAction::triggered, this, [this, tabIndex]() { ui->tabWidget->saveTabAs(tabIndex); }); QAction *pinFileAction = new QAction(this); pinFileAction->setText("Pin File"); connect(pinFileAction, &QAction::triggered, this, [this, tabIndex]() { pinFile(ui->tabWidget->tabPath(tabIndex)); }); if (!ui->tabWidget->tabText(tabIndex).contains("*")) { saveFileAction->setEnabled(false); } QMenu *tabWidgetContextMenu = new QMenu; tabWidgetContextMenu->addAction(closeFileAction); tabWidgetContextMenu->addSeparator(); tabWidgetContextMenu->addAction(saveFileAction); tabWidgetContextMenu->addAction(saveFileAsAction); tabWidgetContextMenu->addSeparator(); tabWidgetContextMenu->addAction(pinFileAction); tabWidgetContextMenu->exec(QCursor::pos()); }