diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 947d3e55..ff01913b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -61,6 +61,7 @@ #include "dialogs/OptionsDialog.h" #include "widgets/EntrypointWidget.h" #include "widgets/DisassemblerGraphView.h" +#include "dialogs/SaveProjectDialog.h" // graphics #include <QGraphicsEllipseItem> @@ -298,16 +299,8 @@ void MainWindow::initUI() QShortcut *refresh_shortcut = new QShortcut(QKeySequence(QKeySequence::Refresh), this); connect(refresh_shortcut, SIGNAL(activated()), this, SLOT(refreshVisibleDockWidgets())); -} -void MainWindow::openFile(const QString &fn, int anal_level, QList<QString> advanced) -{ - QString project_name = qhelpers::uniqueProjectName(fn); - - if (core->getProjectNames().contains(project_name)) - openProject(project_name); - else - openNewFile(fn, anal_level, advanced); + connect(core, SIGNAL(projectSaved(const QString &)), this, SLOT(projectSaved(const QString &))); } void MainWindow::openNewFile(const QString &fn, int anal_level, QList<QString> advanced) @@ -327,7 +320,7 @@ void MainWindow::openProject(const QString &project_name) QString filename = core->cmd("Pi " + project_name); setFilename(filename.trimmed()); - core->cmd("Po " + project_name); + core->openProject(project_name); initUI(); finalizeOpen(); @@ -347,20 +340,9 @@ void MainWindow::finalizeOpen() core->cmd("fs sections"); updateFrames(); - // Restore project notes - QString notes = this->core->cmd("Pnj"); - //qDebug() << "Notes:" << notes; - if (notes != "") + if(core->getNotes().isEmpty()) { - QByteArray ba; - ba.append(notes); - notepadDock->setText(QByteArray::fromBase64(ba)); - } - else - { - addOutput(tr(" > Adding binary information to notepad")); - - notepadDock->setText(tr("# Binary information\n\n") + core->cmd("i") + + core->setNotes(tr("# Binary information\n\n") + core->cmd("i") + "\n" + core->cmd("ie") + "\n" + core->cmd("iM") + "\n"); } @@ -377,14 +359,27 @@ void MainWindow::finalizeOpen() notepadDock->highlightPreview(); } -void MainWindow::saveProject() +bool MainWindow::saveProject(bool quit) { - QString project_name = qhelpers::uniqueProjectName(filename); - core->cmd("Ps " + project_name); - QString notes = this->notepadDock->textToBase64(); - //this->add_debug_output(notes); - this->core->cmd("Pnj " + notes); - this->addOutput(tr("Project saved: ") + project_name); + QString projectName = core->getConfig("prj.name"); + if (projectName.isEmpty()) + { + return saveProjectAs(quit); + } + else + { + core->saveProject(projectName); + return true; + } +} + +bool MainWindow::saveProjectAs(bool quit) +{ + SaveProjectDialog dialog(quit, this); + int result = dialog.exec(); + + return !quit || result != SaveProjectDialog::Rejected; + } void MainWindow::toggleSideBarTheme() @@ -412,13 +407,19 @@ void MainWindow::closeEvent(QCloseEvent *event) //qDebug() << ret; if (ret == QMessageBox::Save) { - QSettings settings; - settings.setValue("geometry", saveGeometry()); - settings.setValue("size", size()); - settings.setValue("pos", pos()); - settings.setValue("state", saveState()); - saveProject(); - QMainWindow::closeEvent(event); + if(saveProject(true)) + { + QSettings settings; + settings.setValue("geometry", saveGeometry()); + settings.setValue("size", size()); + settings.setValue("pos", pos()); + settings.setValue("state", saveState()); + QMainWindow::closeEvent(event); + } + else + { + event->ignore(); + } } else if (ret == QMessageBox::Discard) { @@ -670,7 +671,7 @@ void MainWindow::on_actionRefresh_Panels_triggered() this->updateFrames(); } -void MainWindow::toggleDockWidget(DockWidget *dock_widget) +void MainWindow::toggleDockWidget(QDockWidget *dock_widget) { if (dock_widget->isVisible()) { @@ -808,7 +809,7 @@ void MainWindow::on_actionhide_bottomPannel_triggered() void MainWindow::sendToNotepad(const QString &txt) { - this->notepadDock->appendPlainText("```\n" + txt + "\n```"); + core->setNotes(core->getNotes() + "```\n" + txt + "\n```"); } void MainWindow::on_actionFunctionsRename_triggered() @@ -841,6 +842,11 @@ void MainWindow::on_actionSave_triggered() saveProject(); } +void MainWindow::on_actionSaveAs_triggered() +{ + saveProjectAs(); +} + void MainWindow::on_actionRun_Script_triggered() { QFileDialog dialog(this); @@ -970,3 +976,8 @@ void MainWindow::on_actionAsmOptions_triggered() auto dialog = new AsmOptionsDialog(this); dialog->show(); } + +void MainWindow::projectSaved(const QString &name) +{ + this->addOutput(tr("Project saved: ") + name); +} diff --git a/src/MainWindow.h b/src/MainWindow.h index 87166ceb..c479c5fc 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -53,11 +53,23 @@ public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); - void openFile(const QString &fn, int anal_level = -1, QList<QString> advanced = QList<QString>()); + void openNewFile(const QString &fn, int anal_level = -1, QList<QString> advanced = QList<QString>()); + void openProject(const QString &project_name); + void initUI(); void finalizeOpen(); - void saveProject(); + /*! + * @param quit whether to show destructive button in dialog + * @return if quit is true, false if the application should not close + */ + bool saveProject(bool quit = false); + + /*! + * @param quit whether to show destructive button in dialog + * @return if quit is true, false if the application should not close + */ + bool saveProjectAs(bool quit = false); void start_web_server(); void closeEvent(QCloseEvent *event) override; @@ -144,6 +156,7 @@ private slots: void on_actionNew_triggered(); void on_actionSave_triggered(); + void on_actionSaveAs_triggered(); void on_actionWhite_Theme_triggered(); @@ -165,6 +178,8 @@ private slots: void on_actionAsmOptions_triggered(); + void projectSaved(const QString &name); + private: CutterCore *core; DisassemblyWidget *disassemblyDock; @@ -206,10 +221,7 @@ private: SectionsDock *sectionsDock; ConsoleWidget *consoleWidget; - void openProject(const QString &project_name); - void openNewFile(const QString &fn, int anal_level, QList<QString> advanced); - - void toggleDockWidget(DockWidget *dock_widget); + void toggleDockWidget(QDockWidget *dock_widget); public: RVA getCursorAddress() const { return cursorAddress; } diff --git a/src/MainWindow.ui b/src/MainWindow.ui index b4f70a2f..c8927e80 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -174,7 +174,7 @@ border-top: 0px; <x>0</x> <y>0</y> <width>1013</width> - <height>20</height> + <height>22</height> </rect> </property> <property name="defaultUp"> @@ -186,10 +186,10 @@ border-top: 0px; <widget class="QMenu" name="menuFile"> <property name="geometry"> <rect> - <x>273</x> - <y>136</y> - <width>148</width> - <height>167</height> + <x>419</x> + <y>265</y> + <width>173</width> + <height>206</height> </rect> </property> <property name="title"> @@ -199,6 +199,7 @@ border-top: 0px; <addaction name="actionLoad"/> <addaction name="separator"/> <addaction name="actionSave"/> + <addaction name="actionSaveAs"/> <addaction name="separator"/> <addaction name="actionRun_Script"/> <addaction name="separator"/> @@ -419,6 +420,9 @@ QToolButton .svg-icon path { <property name="text"> <string>Save</string> </property> + <property name="shortcut"> + <string>Ctrl+S</string> + </property> </action> <action name="actionUndo"> <property name="text"> @@ -979,6 +983,11 @@ QToolButton .svg-icon path { <string>Disassembly Options</string> </property> </action> + <action name="actionSaveAs"> + <property name="text"> + <string>Save As...</string> + </property> + </action> </widget> <layoutdefault spacing="6" margin="11"/> <resources> diff --git a/src/cutter.cpp b/src/cutter.cpp index 1f945c4d..f5078fce 100644 --- a/src/cutter.cpp +++ b/src/cutter.cpp @@ -46,7 +46,6 @@ CutterCore::CutterCore(QObject *parent) : QObject(parent) { r_cons_new(); // initialize console - this->projectPath = ""; this->core_ = r_core_new(); r_core_loadlibs(this->core_, R_CORE_LOADLIBS_ALL, NULL); // IMPLICIT r_bin_iobind (core_->bin, core_->io); @@ -1219,3 +1218,31 @@ void CutterCore::loadPDB(const QString &file) { cmd("idp " + sanitizeStringForCommand(file)); } + +void CutterCore::openProject(const QString &name) +{ + cmd("Po " + name); + + QString notes = QString::fromUtf8(QByteArray::fromBase64(cmd("Pnj").toUtf8())); + setNotes(notes); +} + +void CutterCore::saveProject(const QString &name) +{ + cmd("Ps " + name); + cmd("Pnj " + notes.toUtf8().toBase64()); + emit projectSaved(name); +} + +bool CutterCore::isProjectNameValid(const QString &name) +{ + // see is_valid_project_name() in libr/core/project.c + static const QRegExp regexp(R"(^[a-zA-Z0-9\\\._:-]{1,}$)"); + return regexp.exactMatch(name) && !name.endsWith(".zip") ; +} + +void CutterCore::setNotes(const QString ¬es) +{ + this->notes = notes; + emit notesChanged(this->notes); +} diff --git a/src/cutter.h b/src/cutter.h index deffc6d5..da94ac8a 100644 --- a/src/cutter.h +++ b/src/cutter.h @@ -184,8 +184,6 @@ class CutterCore: public QObject friend class ccClass; public: - QString projectPath; - explicit CutterCore(QObject *parent = 0); ~CutterCore(); static CutterCore* getInstance(); @@ -267,6 +265,13 @@ public: QStringList getAnalPluginNames(); QStringList getProjectNames(); + void openProject(const QString &name); + void saveProject(const QString &name); + + static bool isProjectNameValid(const QString &name); + + const QString &getNotes() const { return notes; } + void setNotes(const QString ¬es); QList<RBinPluginDescription> getRBinPluginDescriptions(const QString &type = nullptr); @@ -303,6 +308,9 @@ signals: void flagsChanged(); void commentsChanged(); + void notesChanged(const QString ¬es); + void projectSaved(const QString &name); + /*! * emitted when config regarding disassembly display changes */ @@ -321,6 +329,8 @@ private: QString default_cpu; int default_bits; + QString notes; + RCore *core_; }; diff --git a/src/cutter.pro b/src/cutter.pro index b1c70947..37382962 100644 --- a/src/cutter.pro +++ b/src/cutter.pro @@ -77,7 +77,8 @@ SOURCES += \ widgets/SidebarWidget.cpp \ widgets/HexdumpWidget.cpp \ utils/Configuration.cpp \ - utils/Colors.cpp + utils/Colors.cpp \ + dialogs/SaveProjectDialog.cpp HEADERS += \ cutter.h \ @@ -126,7 +127,8 @@ HEADERS += \ widgets/SidebarWidget.h \ widgets/HexdumpWidget.h \ utils/Configuration.h \ - utils/Colors.h + utils/Colors.h \ + dialogs/SaveProjectDialog.h FORMS += \ widgets/PreviewWidget.ui \ @@ -156,7 +158,8 @@ FORMS += \ widgets/StringsWidget.ui \ widgets/SymbolsWidget.ui \ widgets/SidebarWidget.ui \ - widgets/HexdumpWidget.ui + widgets/HexdumpWidget.ui \ + dialogs/SaveProjectDialog.ui RESOURCES += \ resources.qrc diff --git a/src/dialogs/NewFileDialog.cpp b/src/dialogs/NewFileDialog.cpp index 2c8f3029..34bafbdf 100644 --- a/src/dialogs/NewFileDialog.cpp +++ b/src/dialogs/NewFileDialog.cpp @@ -64,10 +64,152 @@ NewFileDialog::NewFileDialog(QWidget *parent) : { ui->setupUi(this); setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); - ui->recentsList->addAction(ui->actionRemove_item); - ui->recentsList->addAction(ui->actionClear_all); - ui->recentsList->setIconSize(QSize(48, 48)); + ui->recentsListWidget->addAction(ui->actionRemove_item); + ui->recentsListWidget->addAction(ui->actionClear_all); + fillRecentFilesList(); + bool projectsExist = fillProjectsList(); + + if(projectsExist) + { + ui->tabWidget->setCurrentWidget(ui->projectsTab); + } + else + { + ui->tabWidget->setCurrentWidget(ui->filesTab); + } + + // Hide "create" button until the dialog works + ui->createButton->hide(); + + ui->loadProjectButton->setEnabled(ui->projectsListWidget->currentItem() != nullptr); +} + +NewFileDialog::~NewFileDialog() {} + +void NewFileDialog::on_loadFileButton_clicked() +{ + loadFile(ui->newFileEdit->text()); +} + +void NewFileDialog::on_selectFileButton_clicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Select file"), QDir::homePath()); + + if (!fileName.isEmpty()) + { + ui->newFileEdit->setText(fileName); + ui->loadFileButton->setFocus(); + } +} + +void NewFileDialog::on_selectProjectsDirButton_clicked() +{ + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::DirectoryOnly); + + QString currentDir = CutterCore::getInstance()->getConfig("dir.projects"); + if(currentDir.startsWith("~")) + { + currentDir = QDir::homePath() + currentDir.mid(1); + } + dialog.setDirectory(currentDir); + + dialog.setWindowTitle(tr("Select project path (dir.projects)")); + + if(!dialog.exec()) + { + return; + } + + QString dir = dialog.selectedFiles().first(); + if (!dir.isEmpty()) + { + CutterCore::getInstance()->setConfig("dir.projects", dir); + fillProjectsList(); + } +} + +void NewFileDialog::on_loadProjectButton_clicked() +{ + QListWidgetItem *item = ui->projectsListWidget->currentItem(); + + if (item == nullptr) + { + return; + } + + loadProject(item->data(Qt::UserRole).toString()); +} + +void NewFileDialog::on_recentsListWidget_itemClicked(QListWidgetItem *item) +{ + QVariant data = item->data(Qt::UserRole); + QString sitem = data.toString(); + ui->newFileEdit->setText(sitem); +} + +void NewFileDialog::on_recentsListWidget_itemDoubleClicked(QListWidgetItem *item) +{ + loadFile(item->data(Qt::UserRole).toString()); +} + +void NewFileDialog::on_projectsListWidget_itemSelectionChanged() +{ + ui->loadProjectButton->setEnabled(ui->projectsListWidget->currentItem() != nullptr); +} + +void NewFileDialog::on_projectsListWidget_itemDoubleClicked(QListWidgetItem *item) +{ + loadProject(item->data(Qt::UserRole).toString()); +} + +void NewFileDialog::on_cancelButton_clicked() +{ + close(); +} + +void NewFileDialog::on_actionRemove_item_triggered() +{ + // Remove selected item from recents list + QListWidgetItem *item = ui->recentsListWidget->currentItem(); + + QVariant data = item->data(Qt::UserRole); + QString sitem = data.toString(); + + QSettings settings; + QStringList files = settings.value("recentFileList").toStringList(); + files.removeAll(sitem); + settings.setValue("recentFileList", files); + + ui->recentsListWidget->takeItem(ui->recentsListWidget->currentRow()); + + ui->newFileEdit->clear(); +} + +void NewFileDialog::on_createButton_clicked() +{ + // Close dialog and open create new file dialog + close(); + CreateNewDialog *n = new CreateNewDialog(nullptr); + n->exec(); +} + +void NewFileDialog::on_actionClear_all_triggered() +{ + // Clear recent file list + QSettings settings; + QStringList files = settings.value("recentFileList").toStringList(); + files.clear(); + + ui->recentsListWidget->clear(); + // TODO: if called from main window its ok, otherwise its not + settings.setValue("recentFileList", files); + ui->newFileEdit->clear(); +} + +bool NewFileDialog::fillRecentFilesList() +{ // Fill list with recent opened files QSettings settings; @@ -94,30 +236,50 @@ NewFileDialog::NewFileDialog(QWidget *parent) : else { QListWidgetItem *item = new QListWidgetItem( - getIconFor(name, i++), - file + "\nCreated: " + info.created().toString() + "\nSize: " + formatBytecount(info.size()) + getIconFor(name, i++), + file + "\nCreated: " + info.created().toString() + "\nSize: " + formatBytecount(info.size()) ); //":/img/icons/target.svg"), name ); item->setData(Qt::UserRole, file); - ui->recentsList->addItem(item); + ui->recentsListWidget->addItem(item); } } - ui->recentsList->setSortingEnabled(true); - // Hide "create" button until the dialog works - ui->createButton->hide(); - - // Removes files were deleted from the stringlist. Save it again. + // Removed files were deleted from the stringlist. Save it again. settings.setValue("recentFileList", files); + + return !files.isEmpty(); } -NewFileDialog::~NewFileDialog() {} +bool NewFileDialog::fillProjectsList() +{ + CutterCore *core = CutterCore::getInstance(); -void NewFileDialog::on_loadFileButton_clicked() + ui->projectsDirEdit->setText(core->getConfig("dir.projects")); + + QStringList projects = core->getProjectNames(); + projects.sort(Qt::CaseInsensitive); + + ui->projectsListWidget->clear(); + + int i=0; + for(const QString &project : projects) + { + QString info = core->cmd("Pi " + project); + + QListWidgetItem *item = new QListWidgetItem(getIconFor(project, i++), project + "\n" + info); + + item->setData(Qt::UserRole, project); + ui->projectsListWidget->addItem(item); + } + + return !projects.isEmpty(); +} + +void NewFileDialog::loadFile(const QString &filename) { // Check that there is a file selected - QString fname = ui->newFileEdit->text(); - QFileInfo checkfile(fname); + QFileInfo checkfile(filename); if (!checkfile.exists() || !checkfile.isFile()) { QMessageBox msgBox(this); @@ -129,8 +291,8 @@ void NewFileDialog::on_loadFileButton_clicked() // Add file to recent file list QSettings settings; QStringList files = settings.value("recentFileList").toStringList(); - files.removeAll(fname); - files.prepend(fname); + files.removeAll(filename); + files.prepend(filename); while (files.size() > MaxRecentFiles) files.removeLast(); @@ -140,90 +302,16 @@ void NewFileDialog::on_loadFileButton_clicked() // Close dialog and open MainWindow/OptionsDialog MainWindow *main = new MainWindow(); - main->openFile(fname); + main->openNewFile(filename); //OptionsDialog *o = new OptionsDialog(fname); //o->exec(); } } -void NewFileDialog::on_newFileButton_clicked() +void NewFileDialog::loadProject(const QString &project) { - QFileDialog dialog(this); - dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setViewMode(QFileDialog::Detail); - dialog.setDirectory(QDir::home()); - - QString fileName; - fileName = dialog.getOpenFileName(this, tr("Select file")); - - if (!fileName.isEmpty()) - { - ui->newFileEdit->setText(fileName); - ui->loadFileButton->setFocus(); - } -} - -void NewFileDialog::on_recentsList_itemClicked(QListWidgetItem *item) -{ - QVariant data = item->data(Qt::UserRole); - QString sitem = data.toString(); - ui->newFileEdit->setText(sitem); -} - -void NewFileDialog::on_recentsList_itemDoubleClicked(QListWidgetItem *item) -{ - // Get selected item to send to options dialog - QVariant data = item->data(Qt::UserRole); - QString sitem = data.toString(); - // Close dialog and open OptionsDialog close(); MainWindow *main = new MainWindow(); - main->openFile(sitem); - //OptionsDialog *o = new OptionsDialog(sitem); - //o->exec(); -} - -void NewFileDialog::on_cancelButton_clicked() -{ - close(); -} - -void NewFileDialog::on_actionRemove_item_triggered() -{ - // Remove selected item from recents list - QListWidgetItem *item = ui->recentsList->currentItem(); - - QVariant data = item->data(Qt::UserRole); - QString sitem = data.toString(); - - QSettings settings; - QStringList files = settings.value("recentFileList").toStringList(); - files.removeAll(sitem); - settings.setValue("recentFileList", files); - - ui->recentsList->takeItem(ui->recentsList->currentRow()); - - ui->newFileEdit->clear(); -} - -void NewFileDialog::on_createButton_clicked() -{ - // Close dialog and open create new file dialog - close(); - CreateNewDialog *n = new CreateNewDialog(nullptr); - n->exec(); -} - -void NewFileDialog::on_actionClear_all_triggered() -{ - // Clear recent file list - QSettings settings; - QStringList files = settings.value("recentFileList").toStringList(); - files.clear(); - - ui->recentsList->clear(); - // TODO: if called from main window its ok, otherwise its not - settings.setValue("recentFileList", files); - ui->newFileEdit->clear(); + main->openProject(project); } diff --git a/src/dialogs/NewFileDialog.h b/src/dialogs/NewFileDialog.h index fb037d24..654297ec 100644 --- a/src/dialogs/NewFileDialog.h +++ b/src/dialogs/NewFileDialog.h @@ -19,26 +19,40 @@ public: ~NewFileDialog(); private slots: - void on_loadFileButton_clicked(); + void on_selectFileButton_clicked(); + void on_createButton_clicked(); - void on_newFileButton_clicked(); + void on_selectProjectsDirButton_clicked(); + void on_loadProjectButton_clicked(); - void on_recentsList_itemClicked(QListWidgetItem *item); + void on_cancelButton_clicked(); - void on_recentsList_itemDoubleClicked(QListWidgetItem *item); + void on_recentsListWidget_itemClicked(QListWidgetItem *item); + void on_recentsListWidget_itemDoubleClicked(QListWidgetItem *item); - void on_cancelButton_clicked(); + void on_projectsListWidget_itemSelectionChanged(); + void on_projectsListWidget_itemDoubleClicked(QListWidgetItem *item); void on_actionRemove_item_triggered(); - - void on_createButton_clicked(); - void on_actionClear_all_triggered(); private: std::unique_ptr<Ui::NewFileDialog> ui; + /*! + * @return true if list is not empty + */ + bool fillRecentFilesList(); + + /*! + * @return true if list is not empty + */ + bool fillProjectsList(); + + void loadFile(const QString &filename); + void loadProject(const QString &project); + static const int MaxRecentFiles = 5; }; diff --git a/src/dialogs/NewfileDialog.ui b/src/dialogs/NewfileDialog.ui index 388e4c1d..679df1ce 100644 --- a/src/dialogs/NewfileDialog.ui +++ b/src/dialogs/NewfileDialog.ui @@ -6,15 +6,15 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>450</height> + <width>452</width> + <height>532</height> </rect> </property> <property name="windowTitle"> <string>Open File</string> </property> <property name="windowIcon"> - <iconset resource="resources.qrc"> + <iconset> <normaloff>:/img/logo-small.png</normaloff>:/img/logo-small.png</iconset> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -42,7 +42,7 @@ <string/> </property> <property name="pixmap"> - <pixmap resource="resources.qrc">:/img/logo-small.png</pixmap> + <pixmap>:/img/logo-small.png</pixmap> </property> <property name="scaledContents"> <bool>false</bool> @@ -97,123 +97,309 @@ <number>5</number> </property> <item> - <layout class="QGridLayout" name="gridLayout"> - <property name="sizeConstraint"> - <enum>QLayout::SetDefaultConstraint</enum> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> </property> - <property name="horizontalSpacing"> - <number>5</number> - </property> - <item row="3" column="0" colspan="2"> - <widget class="Line" name="line"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>1</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item row="5" column="0" colspan="2"> - <widget class="QListWidget" name="recentsList"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>1</verstretch> - </sizepolicy> - </property> - <property name="font"> - <font> - <pointsize>11</pointsize> - </font> - </property> - <property name="contextMenuPolicy"> - <enum>Qt::ActionsContextMenu</enum> - </property> - <property name="frameShape"> - <enum>QFrame::NoFrame</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Plain</enum> - </property> - <property name="lineWidth"> - <number>0</number> - </property> - <property name="verticalScrollMode"> - <enum>QAbstractItemView::ScrollPerPixel</enum> - </property> - <property name="resizeMode"> - <enum>QListView::Adjust</enum> - </property> - <property name="spacing"> - <number>5</number> - </property> - <property name="viewMode"> - <enum>QListView::ListMode</enum> - </property> - <property name="uniformItemSizes"> - <bool>false</bool> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> - <property name="selectionRectVisible"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QPushButton" name="newFileButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Select</string> - </property> - </widget> - </item> - <item row="4" column="0" colspan="2"> - <widget class="QLabel" name="loadFileLabel"> - <property name="text"> - <string><html><head/><body><p><span style=" font-weight:600;">Previous sessions</span></p></body></html></string> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLineEdit" name="newFileEdit"> - <property name="font"> - <font> - <pointsize>12</pointsize> - </font> - </property> - <property name="frame"> - <bool>false</bool> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="newFileLabel"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string><html><head/><body><p><span style=" font-weight:600;">Select new file</span></p></body></html></string> - </property> - </widget> - </item> - </layout> + <widget class="QWidget" name="filesTab"> + <attribute name="title"> + <string>Open File</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <item row="1" column="1"> + <widget class="QPushButton" name="selectFileButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Select</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="newFileEdit"> + <property name="frame"> + <bool>false</bool> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="newFileLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600;">Select new file</span></p></body></html></string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="recentsListWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>11</pointsize> + </font> + </property> + <property name="contextMenuPolicy"> + <enum>Qt::ActionsContextMenu</enum> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="resizeMode"> + <enum>QListView::Adjust</enum> + </property> + <property name="spacing"> + <number>5</number> + </property> + <property name="viewMode"> + <enum>QListView::ListMode</enum> + </property> + <property name="uniformItemSizes"> + <bool>false</bool> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + <property name="selectionRectVisible"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="createButton"> + <property name="text"> + <string>Create</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="loadFileButton"> + <property name="text"> + <string>Open</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="projectsTab"> + <attribute name="title"> + <string>Projects</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <item row="1" column="1"> + <widget class="QPushButton" name="selectProjectsDirButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Select</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="projectsDirEdit"> + <property name="frame"> + <bool>false</bool> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="projectsDirLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600;">Projects path (dir.projects):</span></p></body></html></string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="projectsListWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>11</pointsize> + </font> + </property> + <property name="contextMenuPolicy"> + <enum>Qt::ActionsContextMenu</enum> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="resizeMode"> + <enum>QListView::Adjust</enum> + </property> + <property name="spacing"> + <number>5</number> + </property> + <property name="viewMode"> + <enum>QListView::ListMode</enum> + </property> + <property name="uniformItemSizes"> + <bool>false</bool> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + <property name="selectionRectVisible"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="loadProjectButton"> + <property name="text"> + <string>Open</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </widget> </item> </layout> </widget> @@ -221,7 +407,7 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="buttonBar" stretch="0,0,0,0"> + <layout class="QHBoxLayout" name="buttonBar" stretch="0,0"> <property name="spacing"> <number>10</number> </property> @@ -248,20 +434,6 @@ </property> </spacer> </item> - <item> - <widget class="QPushButton" name="createButton"> - <property name="text"> - <string>Create</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="loadFileButton"> - <property name="text"> - <string>Open</string> - </property> - </widget> - </item> </layout> </item> </layout> @@ -277,7 +449,8 @@ </action> </widget> <resources> - <include location="resources.qrc"/> + <include location="../resources.qrc"/> + <include location="../resources.qrc"/> </resources> <connections/> </ui> diff --git a/src/dialogs/SaveProjectDialog.cpp b/src/dialogs/SaveProjectDialog.cpp new file mode 100644 index 00000000..6aba072d --- /dev/null +++ b/src/dialogs/SaveProjectDialog.cpp @@ -0,0 +1,104 @@ + +#include <QFileDialog> + +#include <cutter.h> +#include "SaveProjectDialog.h" +#include "ui_SaveProjectDialog.h" + +SaveProjectDialog::SaveProjectDialog(bool quit, QWidget *parent) : + QDialog(parent), + ui(new Ui::SaveProjectDialog) +{ + ui->setupUi(this); + + CutterCore *core = CutterCore::getInstance(); + + if (quit) + { + ui->buttonBox->setStandardButtons(QDialogButtonBox::Save + | QDialogButtonBox::Discard + | QDialogButtonBox::Cancel); + } + else + { + ui->buttonBox->setStandardButtons(QDialogButtonBox::Save + | QDialogButtonBox::Cancel); + } + + ui->nameEdit->setText(core->getConfig("prj.name")); + ui->projectsDirEdit->setText(core->getConfig("dir.projects")); + ui->filesCheckBox->setChecked(core->getConfigb("prj.files")); + ui->gitCheckBox->setChecked(core->getConfigb("prj.git")); + ui->zipCheckBox->setChecked(core->getConfigb("prj.zip")); +} + +SaveProjectDialog::~SaveProjectDialog() +{ +} + +void SaveProjectDialog::on_selectProjectsDirButton_clicked() +{ + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::DirectoryOnly); + + QString currentDir = ui->projectsDirEdit->text(); + if(currentDir.startsWith("~")) + { + currentDir = QDir::homePath() + currentDir.mid(1); + } + dialog.setDirectory(currentDir); + + dialog.setWindowTitle(tr("Select project path (dir.projects)")); + + if(!dialog.exec()) + { + return; + } + + QString dir = dialog.selectedFiles().first(); + if (!dir.isEmpty()) + { + ui->projectsDirEdit->setText(dir); + } +} + +void SaveProjectDialog::on_buttonBox_clicked(QAbstractButton *button) +{ + switch(ui->buttonBox->buttonRole(button)) + { + case QDialogButtonBox::DestructiveRole: + QDialog::done(Destructive); + break; + + case QDialogButtonBox::RejectRole: + QDialog::done(Rejected); + break; + + default: + break; + } +} + +void SaveProjectDialog::accept() +{ + CutterCore *core = CutterCore::getInstance(); + core->setConfig("dir.projects", ui->projectsDirEdit->text().toUtf8().constData()); + core->setConfig("prj.files", ui->filesCheckBox->isChecked()); + core->setConfig("prj.git", ui->gitCheckBox->isChecked()); + core->setConfig("prj.zip", ui->zipCheckBox->isChecked()); + + QString projectName = ui->nameEdit->text().trimmed(); + if(!CutterCore::isProjectNameValid(projectName)) + { + QMessageBox::critical(this, tr("Save project"), tr("Invalid project name.")); + return; + } + + core->saveProject(projectName); + + QDialog::done(Saved); +} + +void SaveProjectDialog::reject() +{ +} diff --git a/src/dialogs/SaveProjectDialog.h b/src/dialogs/SaveProjectDialog.h new file mode 100644 index 00000000..f4fed151 --- /dev/null +++ b/src/dialogs/SaveProjectDialog.h @@ -0,0 +1,35 @@ +#ifndef SAVEPROJECTDIALOG_H +#define SAVEPROJECTDIALOG_H + +#include <QDialog> +#include <QAbstractButton> + +#include <memory> + +namespace Ui +{ + class SaveProjectDialog; +} + +class SaveProjectDialog : public QDialog +{ + Q_OBJECT + +public: + enum Result { Saved, Rejected, Destructive }; + + explicit SaveProjectDialog(bool quit, QWidget *parent = 0); + ~SaveProjectDialog(); + + virtual void accept() override; + virtual void reject() override; + +private slots: + void on_buttonBox_clicked(QAbstractButton *button); + void on_selectProjectsDirButton_clicked(); + +private: + std::unique_ptr<Ui::SaveProjectDialog> ui; +}; + +#endif // SAVEPROJECTDIALOG_H diff --git a/src/dialogs/SaveProjectDialog.ui b/src/dialogs/SaveProjectDialog.ui new file mode 100644 index 00000000..77b77174 --- /dev/null +++ b/src/dialogs/SaveProjectDialog.ui @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SaveProjectDialog</class> + <widget class="QDialog" name="SaveProjectDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>506</width> + <height>227</height> + </rect> + </property> + <property name="windowTitle"> + <string>Save Project</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetMinAndMaxSize</enum> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>Project name (prj.name):</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nameEdit"/> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="nameLabel_2"> + <property name="text"> + <string>Projects path (dir.projects):</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="projectsDirEdit"> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="selectProjectsDirButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Select</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="filesCheckBox"> + <property name="text"> + <string>Save the target binary inside the project directory (prj.files)</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="gitCheckBox"> + <property name="text"> + <string>Project is a git repo and saving is committing (prj.git)</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="zipCheckBox"> + <property name="text"> + <string>Use ZIP format for project files (prj.zip)</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SaveProjectDialog</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>SaveProjectDialog</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> diff --git a/src/main.cpp b/src/main.cpp index 0d0cbf13..d5890aec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -82,7 +82,7 @@ int main(int argc, char *argv[]) else // filename specified as positional argument { MainWindow *main = new MainWindow(); - main->openFile(args[0], anal_level_specified ? anal_level : -1); + main->openNewFile(args[0], anal_level_specified ? anal_level : -1); } return a.exec(); diff --git a/src/utils/Helpers.cpp b/src/utils/Helpers.cpp index cfd79cf9..080ac672 100644 --- a/src/utils/Helpers.cpp +++ b/src/utils/Helpers.cpp @@ -39,12 +39,6 @@ namespace qhelpers #endif } - QString uniqueProjectName(const QString &filename) - { - const QByteArray fullHash(QCryptographicHash::hash(filename.toUtf8(), QCryptographicHash::Sha1)); - return QFileInfo(filename).fileName() + "_" + fullHash.toHex().left(10); - } - void adjustColumns(QTreeWidget *tw, int columnCount, int padding) { const int count = columnCount == 0 ? tw->columnCount() : columnCount; diff --git a/src/utils/Helpers.h b/src/utils/Helpers.h index 19a16fbd..fba10672 100644 --- a/src/utils/Helpers.h +++ b/src/utils/Helpers.h @@ -16,8 +16,6 @@ namespace qhelpers void normalizeFont(QPlainTextEdit *edit); void normalizeEditFont(QTextEdit *edit); - QString uniqueProjectName(const QString &filename); - void adjustColumns(QTreeWidget *tw, int columnCount = 0, int padding = 0); QTreeWidgetItem *appendRow(QTreeWidget *tw, const QString &str, const QString &str2 = QString(), diff --git a/src/widgets/Notepad.cpp b/src/widgets/Notepad.cpp index 2fb0c44d..a698e417 100644 --- a/src/widgets/Notepad.cpp +++ b/src/widgets/Notepad.cpp @@ -15,7 +15,7 @@ Notepad::Notepad(MainWindow *main, QWidget *parent) : - DockWidget(parent), + QDockWidget(parent), ui(new Ui::Notepad) { ui->setupUi(this); @@ -40,35 +40,15 @@ Notepad::Notepad(MainWindow *main, QWidget *parent) : ui->notepadTextEdit->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->notepadTextEdit, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showNotepadContextMenu(const QPoint &))); + connect(ui->notepadTextEdit, SIGNAL(textChanged()), this, SLOT(textChanged())); + + connect(CutterCore::getInstance(), SIGNAL(notesChanged(const QString &)), this, SLOT(updateNotes(const QString &))); + + updateNotes(CutterCore::getInstance()->getNotes()); } Notepad::~Notepad() {} -void Notepad::setup() -{ -} - -void Notepad::refresh() -{ - // TODO: implement - eprintf("%s - not implemented\n", Q_FUNC_INFO); -} - -void Notepad::setText(const QString &str) -{ - ui->notepadTextEdit->setPlainText(str); -} - -QString Notepad::textToBase64() const -{ - return notesTextEdit->toPlainText().toUtf8().toBase64(); -} - -void Notepad::appendPlainText(const QString &text) -{ - notesTextEdit->appendPlainText(text); -} - void Notepad::on_fontButton_clicked() { bool ok = true; @@ -359,3 +339,18 @@ void Notepad::on_actionHexdump_function_triggered() { ui->previewTextEdit->setPlainText(CutterCore::getInstance()->cmd("pxf @ " + this->addr)); } + +void Notepad::updateNotes(const QString ¬es) +{ + disconnect(ui->notepadTextEdit, SIGNAL(textChanged()), this, SLOT(textChanged())); + ui->notepadTextEdit->setPlainText(notes); + connect(ui->notepadTextEdit, SIGNAL(textChanged()), this, SLOT(textChanged())); +} + +void Notepad::textChanged() +{ + CutterCore *core = CutterCore::getInstance(); + disconnect(core, SIGNAL(notesChanged(const QString &)), this, SLOT(updateNotes(const QString &))); + core->setNotes(ui->notepadTextEdit->toPlainText()); + connect(core, SIGNAL(notesChanged(const QString &)), this, SLOT(updateNotes(const QString &))); +} diff --git a/src/widgets/Notepad.h b/src/widgets/Notepad.h index 2dd8e7a0..5a5b1cf8 100644 --- a/src/widgets/Notepad.h +++ b/src/widgets/Notepad.h @@ -1,8 +1,8 @@ #ifndef NOTEPAD_H #define NOTEPAD_H -#include "DockWidget.h" #include <memory> +#include <QDockWidget> class MainWindow; class MdHighlighter; @@ -14,7 +14,7 @@ namespace Ui class Notepad; } -class Notepad : public DockWidget +class Notepad : public QDockWidget { Q_OBJECT @@ -22,15 +22,6 @@ public: explicit Notepad(MainWindow *main, QWidget *parent = 0); ~Notepad(); - void setup() override; - - void refresh() override; - - void setText(const QString &str); - QString textToBase64() const; - - void appendPlainText(const QString &text); - void highlightPreview(); public slots: @@ -38,39 +29,31 @@ public slots: private slots: void on_fontButton_clicked(); - void on_boldButton_clicked(); - void on_italicsButton_clicked(); void on_h1Button_clicked(); - void on_h2Button_clicked(); - void on_h3Button_clicked(); void on_undoButton_clicked(); - void on_redoButton_clicked(); void on_searchEdit_returnPressed(); - void on_searchEdit_textEdited(const QString &arg1); - void on_searchEdit_textChanged(const QString &arg1); void showNotepadContextMenu(const QPoint &pt); void on_actionDisassmble_bytes_triggered(); - void on_actionDisassmble_function_triggered(); - void on_actionHexdump_bytes_triggered(); - void on_actionCompact_Hexdump_triggered(); - void on_actionHexdump_function_triggered(); + void updateNotes(const QString ¬es); + void textChanged(); + private: std::unique_ptr<Ui::Notepad> ui; MdHighlighter *highlighter;