diff --git a/3rdparty/QConsoleWidget/QConsoleWidget.cpp b/3rdparty/QConsoleWidget/QConsoleWidget.cpp index 8616eee..695d12e 100644 --- a/3rdparty/QConsoleWidget/QConsoleWidget.cpp +++ b/3rdparty/QConsoleWidget/QConsoleWidget.cpp @@ -1,3 +1,20 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + #include "QConsoleWidget.h" #include "QConsoleIODevice.h" #include "wingsyntaxhighlighter.h" diff --git a/3rdparty/QConsoleWidget/QConsoleWidget.h b/3rdparty/QConsoleWidget/QConsoleWidget.h index 28ea7ef..82af016 100644 --- a/3rdparty/QConsoleWidget/QConsoleWidget.h +++ b/3rdparty/QConsoleWidget/QConsoleWidget.h @@ -1,3 +1,20 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + #ifndef _QCONSOLEWIDGET_H_ #define _QCONSOLEWIDGET_H_ diff --git a/3rdparty/QConsoleWidget/commandhistorymanager.cpp b/3rdparty/QConsoleWidget/commandhistorymanager.cpp index 13e4665..537b327 100644 --- a/3rdparty/QConsoleWidget/commandhistorymanager.cpp +++ b/3rdparty/QConsoleWidget/commandhistorymanager.cpp @@ -1,3 +1,20 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + #include "commandhistorymanager.h" #include "utilities.h" diff --git a/3rdparty/QConsoleWidget/commandhistorymanager.h b/3rdparty/QConsoleWidget/commandhistorymanager.h index 1800e0b..28a9d84 100644 --- a/3rdparty/QConsoleWidget/commandhistorymanager.h +++ b/3rdparty/QConsoleWidget/commandhistorymanager.h @@ -1,3 +1,20 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + #ifndef COMMANDHISTORYMANAGER_H #define COMMANDHISTORYMANAGER_H diff --git a/3rdparty/QHexView/document/buffer/qhexbuffer.cpp b/3rdparty/QHexView/document/buffer/qhexbuffer.cpp index 9780ef0..8b77eaf 100644 --- a/3rdparty/QHexView/document/buffer/qhexbuffer.cpp +++ b/3rdparty/QHexView/document/buffer/qhexbuffer.cpp @@ -24,7 +24,10 @@ QHexBuffer::QHexBuffer(QObject *parent) : QObject(parent) {} -uchar QHexBuffer::at(qsizetype idx) { return uchar(this->read(idx, 1)[0]); } +uchar QHexBuffer::at(qsizetype idx) { + auto data = this->read(idx, 1); + return uchar(data[0]); +} bool QHexBuffer::isEmpty() const { return this->length() <= 0; } diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a8910..d655ab6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,7 +175,11 @@ set(DIALOG_SRC src/dialog/crashreport.cpp src/dialog/crashreport.ui src/dialog/showtextdialog.cpp - src/dialog/showtextdialog.h) + src/dialog/showtextdialog.h + src/dialog/definitiondownload.cpp + src/dialog/definitiondownload.h + src/dialog/searchdialog.cpp + src/dialog/searchdialog.h) set(CONTROL_SRC src/control/codeedit.h diff --git a/images/defines.png b/images/defines.png new file mode 100644 index 0000000..9745a2e Binary files /dev/null and b/images/defines.png differ diff --git a/images/search.png b/images/search.png new file mode 100644 index 0000000..ac85e0c Binary files /dev/null and b/images/search.png differ diff --git a/lang/zh_CN/winghex_zh_CN.ts b/lang/zh_CN/winghex_zh_CN.ts index 88366fa..c31140a 100644 --- a/lang/zh_CN/winghex_zh_CN.ts +++ b/lang/zh_CN/winghex_zh_CN.ts @@ -232,7 +232,7 @@ <html><head/><body><p><span style=" font-size:12pt; font-weight:700;">哎呀!程序崩溃了。请将下面的信息发送给开发者寻求帮助:</span></p></body></html> - + CopyToClipBoard 数据已拷贝到粘贴板 @@ -268,6 +268,44 @@ + + DefinitionDownloadDialog + + + Update Syntax Definitions + + + + + Updating syntax definitions from online repository... + + + + + %1:%2 hours + + + + + %1:%2 minutes + + + + + %1.%2 seconds + + + + + %1 ms + + + + + Update operation completed (%1) + + + DockWidgetTab @@ -1235,7 +1273,7 @@ - + NothingToSave 没有保存的数据 @@ -1399,7 +1437,7 @@ - + LayoutRestoring... 恢复布局中... @@ -1410,7 +1448,7 @@ - + SaveLayout 保存布局 @@ -1521,13 +1559,13 @@ - - - - - - - + + + + + + + Error 错误 @@ -1544,8 +1582,8 @@ - - + + FilePermission 因文件权限无法继续! @@ -1571,7 +1609,7 @@ - + ChooseSaveFile 请选择保存文件路径: @@ -1591,42 +1629,42 @@ 搜索数量已到达上限,结果可能不全,建议请按区段搜索。 - + SaveLayoutSuccess 保存布局成功 - + SaveLayoutError 保存布局失败 - + HasClonedView 该编辑页已被克隆编辑,如果关闭,相关联的页也会被关闭,你确认继续吗? - + SaveWorkSpace 保存工作区 - + WingHexWorkSpace (*.wingpro) 羽云十六进制工作区 (*.wingpro) - + ConfirmSave 正在关闭未保存的文件或工作区,你确定保存吗? - + Column %1 列 %1 - + ConfirmAPPSave 你尝试关闭程序,但仍存在未保存的文件或工作区,你确定保存这些更改吗? @@ -1636,7 +1674,7 @@ - + SaveSuccessfully 保存成功! @@ -1647,7 +1685,7 @@ 保存工作区错误! - + Warn 警告 @@ -1820,7 +1858,7 @@ - + NoSelection 没有选区,无法继续的操作! @@ -1876,83 +1914,83 @@ 超出解码字节限制…… - + LayoutSaving... 布局保存中... - + PleaseInput 请输入 - + LogExporting... 日志导出中... - + ExportLogError 导出日志失败! - + ExportLogSuccess 导出日志成功,路径: - + ClearLogSuccess 清空日志成功! - + BadNetwork 无法与远程服务器的更新检查建立连接,请检查网络。 - + NewestVersion 当前软件为最新版本 - + OlderVersion 你使用的软件为老版本,建议到 Github 和 Gitee 的仓库发行版下载更新。 - + CheckingUpdate 检查更新中…… - + Too much opened files 打开的文件过多,无法继续操作! - + FilePermissionSure2Quit 因文件权限无法保存,你确认要退出吗? - + UnknownErrorSure2Quit 因未知错误无法保存,你确认要退出吗? - + WorkSpaceUnSavedSure2Quit 工作区文件无法保存,你确认要退出吗? - + CopyLimit 拷贝字节超出限制 - + ErrOpenFileBelow 打开文件出现错误(由于权限不足),如下为打开错误的文件: @@ -2477,32 +2515,32 @@ 制表符宽度 - + Default 默认 - + IndentMixed 混合缩进 - + IndentSpaces 空格缩进 - + Console 控制台 - + IndentTabs 制表符缩进 - + Edit 编辑 @@ -4386,22 +4424,22 @@ ScriptingConsole - + [Info] 【信息】 - + [Warn] 【警告】 - + [Error] 【错误】 - + [Console] 【控制台】 @@ -4835,6 +4873,143 @@ 关于 QT + + SearchDialog + + + Find and Replace... + + + + + Match ca&se + + + + + Match &whole words + + + + + Regular e&xpressions + + + + + &Escape sequences + + + + + Wrap Aro&und + + + + + Find &Next + + + + + Find &Previous + + + + + &Replace + + + + + Replace &All + + + + + &In Selection + + + + + &Close + + + + + &Find: + + + + + Replace wit&h: + + + + + + The specified text was not found + + + + + The specified text was not found in the selection + + + + + Successfully replaced %1 matches + + + + + SearchWidget + + + Search Settings + + + + + Match ca&se + + + + + Match &whole words + + + + + Regular e&xpressions + + + + + &Escape sequences + + + + + Wrap Aro&und + + + + + Find Next + + + + + Find Previous + + + + + The specified text was not found + + + SettingDialog @@ -4877,57 +5052,63 @@ ShowTextDialog - + General 基本 - + Copy 复制 - + TextBroswer 文本浏览 + Plain Text + 纯文本 + + + TooHugeFile 文件内容过大 - + + + Loading... + + + + Edit 编辑 - - View - 视图 - - - + Find 查找 - + Goto 跳转 - + Encoding 编码 - - Display - 显示 + + UpdateDefs + - + CopyToClipBoard 数据已拷贝到粘贴板 @@ -4948,7 +5129,7 @@ SyntaxPopup - + Plain Text 纯文本 diff --git a/lang/zh_TW/winghex_zh_TW.ts b/lang/zh_TW/winghex_zh_TW.ts index 7a97141..05967c1 100644 --- a/lang/zh_TW/winghex_zh_TW.ts +++ b/lang/zh_TW/winghex_zh_TW.ts @@ -232,7 +232,7 @@ <html><head/><body><p><span style=" font-size:12pt; font-weight:700;">哎呀!程式崩潰了。請將下麵的資訊發送給開發者尋求幫助:</span></p></body></html> - + CopyToClipBoard 數據已拷貝到粘貼板 @@ -268,6 +268,44 @@ + + DefinitionDownloadDialog + + + Update Syntax Definitions + + + + + Updating syntax definitions from online repository... + + + + + %1:%2 hours + + + + + %1:%2 minutes + + + + + %1.%2 seconds + + + + + %1 ms + + + + + Update operation completed (%1) + + + DockWidgetTab @@ -1235,7 +1273,7 @@ - + NothingToSave 沒有保存的數據 @@ -1399,7 +1437,7 @@ - + LayoutRestoring... 恢復佈局中... @@ -1410,7 +1448,7 @@ - + SaveLayout 保存佈局 @@ -1521,13 +1559,13 @@ - - - - - - - + + + + + + + Error 錯誤 @@ -1544,8 +1582,8 @@ - - + + FilePermission 因檔許可權無法繼續! @@ -1571,7 +1609,7 @@ - + ChooseSaveFile 請選擇保存檔路徑: @@ -1591,42 +1629,42 @@ 搜索數量已到達上限,結果可能不全,建議請按區段搜索。 - + SaveLayoutSuccess 保存佈局成功 - + SaveLayoutError 保存佈局失敗 - + HasClonedView 該編輯頁已被克隆編輯,如果關閉,相關聯的頁也會被關閉,你確認繼續嗎? - + SaveWorkSpace 保存工作區 - + WingHexWorkSpace (*.wingpro) 羽雲十六進制工作區 (*.wingpro) - + ConfirmSave 正在關閉未保存的檔或工作區,你確定保存嗎? - + Column %1 列 %1 - + ConfirmAPPSave 你嘗試關閉程式,但仍存在未保存的檔或工作區,你確定保存這些更改嗎? @@ -1636,7 +1674,7 @@ - + SaveSuccessfully 保存成功! @@ -1647,7 +1685,7 @@ 保存工作區錯誤! - + Warn 警告 @@ -1820,7 +1858,7 @@ - + NoSelection 沒有選區,無法繼續的操作! @@ -1876,83 +1914,83 @@ 超出解碼位元組限制…… - + LayoutSaving... 佈局保存中... - + PleaseInput 請輸入 - + LogExporting... 日誌導出中... - + ExportLogError 導出日誌失敗! - + ExportLogSuccess 導出日誌成功,路徑: - + ClearLogSuccess 清空日誌成功! - + BadNetwork 無法與遠程伺服器的更新檢查建立連接,請檢查網路。 - + NewestVersion 當前軟體為最新版本 - + OlderVersion 你使用的軟體為老版本,建議到 Github 和 Gitee 的倉庫發行版下載更新。 - + CheckingUpdate 檢查更新中…… - + Too much opened files 打開的檔過多,無法繼續操作! - + FilePermissionSure2Quit 因檔許可權無法保存,你確認要退出嗎? - + UnknownErrorSure2Quit 因未知錯誤無法保存,你確認要退出嗎? - + WorkSpaceUnSavedSure2Quit 工作區檔無法保存,你確認要退出嗎? - + CopyLimit 拷貝位元組超出限制 - + ErrOpenFileBelow 打開檔出現錯誤(由於許可權不足),如下為打開錯誤的檔: @@ -2477,32 +2515,32 @@ 跳位字元寬度 - + Default 默認 - + IndentMixed 混合縮進 - + IndentSpaces 空格縮進 - + Console 控制台 - + IndentTabs 定位字元縮進 - + Edit 編輯 @@ -4386,22 +4424,22 @@ ScriptingConsole - + [Info] 【資訊】 - + [Warn] 【警告】 - + [Error] 【錯誤】 - + [Console] 【控制臺】 @@ -4835,6 +4873,143 @@ 關於 QT + + SearchDialog + + + Find and Replace... + + + + + Match ca&se + + + + + Match &whole words + + + + + Regular e&xpressions + + + + + &Escape sequences + + + + + Wrap Aro&und + + + + + Find &Next + + + + + Find &Previous + + + + + &Replace + + + + + Replace &All + + + + + &In Selection + + + + + &Close + + + + + &Find: + + + + + Replace wit&h: + + + + + + The specified text was not found + + + + + The specified text was not found in the selection + + + + + Successfully replaced %1 matches + + + + + SearchWidget + + + Search Settings + + + + + Match ca&se + + + + + Match &whole words + + + + + Regular e&xpressions + + + + + &Escape sequences + + + + + Wrap Aro&und + + + + + Find Next + + + + + Find Previous + + + + + The specified text was not found + + + SettingDialog @@ -4877,57 +5052,63 @@ ShowTextDialog - + General 基本 - + Copy 複製 - + TextBroswer 文本流覽 + Plain Text + 純文字 + + + TooHugeFile 檔內容過大 - + + + Loading... + + + + Edit 編輯 - - View - 視圖 - - - + Find 查找 - + Goto 跳轉 - + Encoding 編碼 - - Display - 顯示 + + UpdateDefs + - + CopyToClipBoard 數據已拷貝到粘貼板 @@ -4948,7 +5129,7 @@ SyntaxPopup - + Plain Text 純文字 diff --git a/resources.qrc b/resources.qrc index b75d821..c385d16 100644 --- a/resources.qrc +++ b/resources.qrc @@ -27,6 +27,7 @@ images/dbgstepout.png images/dbgstepover.png images/dbgstop.png + images/defines.png images/del.png images/devext.png images/edit.png @@ -126,6 +127,7 @@ images/paste.png images/redo.png images/replace.png + images/search.png images/undo.png diff --git a/src/angelscript.xml b/src/angelscript.xml index fb57029..d3f5967 100644 --- a/src/angelscript.xml +++ b/src/angelscript.xml @@ -109,7 +109,6 @@ - @@ -132,8 +131,8 @@ - - + + diff --git a/src/control/codeedit.cpp b/src/control/codeedit.cpp index efa3936..414d420 100644 --- a/src/control/codeedit.cpp +++ b/src/control/codeedit.cpp @@ -17,11 +17,17 @@ #include "codeedit.h" #include "class/scriptsettings.h" +#include "class/skinmanager.h" #include "model/codecompletionmodel.h" +#include #include #include +#include +#include +#include + CodeEdit::CodeEdit(QWidget *parent) : WingCodeEdit(parent) { connect(this->document(), &QTextDocument::modificationChanged, this, &CodeEdit::contentModified); @@ -30,6 +36,28 @@ CodeEdit::CodeEdit(QWidget *parent) : WingCodeEdit(parent) { connect(&ScriptSettings::instance(), &ScriptSettings::editorSettingsUpdate, this, &CodeEdit::applyEditorSetStyle); applyEditorSetStyle(); + + m_searchWidget = new SearchWidget(this, parent); + showSearchBar(false); + + connect(this, &CodeEdit::textChanged, this, [this] { + if (m_searchWidget->isVisible()) + showSearchBar(false); + }); +} + +void CodeEdit::showSearchBar(bool show) { + m_searchWidget->setVisible(show); + m_searchWidget->setEnabled(show); + if (show) { + const QTextCursor cursor = textCursor(); + if (cursor.hasSelection()) + m_searchWidget->setSearchText(cursor.selectedText()); + m_searchWidget->activate(); + } else { + clearLiveSearch(); + setFocus(Qt::OtherFocusReason); + } } void CodeEdit::onCompletion(const QModelIndex &index) { @@ -65,6 +93,23 @@ void CodeEdit::applyEditorSetStyle() { auto &set = ScriptSettings::instance(); auto dfont = QFont(set.editorFontFamily()); dfont.setPointSize(set.editorFontSize()); + + auto thname = set.editorTheme(); + if (thname.isEmpty()) { + switch (SkinManager::instance().currentTheme()) { + case SkinManager::Theme::Dark: + this->setTheme(syntaxRepo().defaultTheme( + KSyntaxHighlighting::Repository::DarkTheme)); + break; + case SkinManager::Theme::Light: + this->setTheme(syntaxRepo().defaultTheme( + KSyntaxHighlighting::Repository::LightTheme)); + break; + } + } else { + this->setTheme(syntaxRepo().theme(thname)); + } + this->setFont(dfont); this->setTabWidth(set.editorTabWidth()); this->setIndentationMode(WingCodeEdit::IndentationMode(set.editorInden())); @@ -77,3 +122,16 @@ void CodeEdit::applyEditorSetStyle() { this->setShowWhitespace(set.editorShowWhiteSpace()); this->setAutoCloseChar(set.editorAutoCloseChar()); } + +void CodeEdit::resizeEvent(QResizeEvent *event) { + if (event) + WingCodeEdit::resizeEvent(event); + + // Move the search widget to the upper-right corner + const QPoint editorPos = this->pos(); + QSize searchSize = m_searchWidget->sizeHint(); + m_searchWidget->resize(searchSize); + m_searchWidget->move(editorPos.x() + this->viewport()->width() - + searchSize.width() - 16, + editorPos.y()); +} diff --git a/src/control/codeedit.h b/src/control/codeedit.h index 7bf83a7..1dfdb14 100644 --- a/src/control/codeedit.h +++ b/src/control/codeedit.h @@ -19,6 +19,7 @@ #define CODEEDIT_H #include "WingCodeEdit/wingcodeedit.h" +#include "dialog/searchdialog.h" class CodeEdit : public WingCodeEdit { Q_OBJECT @@ -26,6 +27,9 @@ class CodeEdit : public WingCodeEdit { public: explicit CodeEdit(QWidget *parent = nullptr); +public: + void showSearchBar(bool show); + signals: void contentModified(bool b); @@ -35,8 +39,15 @@ protected slots: private: void addMoveLineShortCut(); + // QWidget interface +protected: + virtual void resizeEvent(QResizeEvent *event) override; + private slots: void applyEditorSetStyle(); + +private: + SearchWidget *m_searchWidget; }; #endif // CODEEDIT_H diff --git a/src/control/scriptingconsole.cpp b/src/control/scriptingconsole.cpp index 588a003..4e4f06a 100644 --- a/src/control/scriptingconsole.cpp +++ b/src/control/scriptingconsole.cpp @@ -20,6 +20,7 @@ #include "class/logger.h" #include "class/scriptconsolemachine.h" #include "class/scriptsettings.h" +#include "class/skinmanager.h" #include "model/codecompletionmodel.h" #include @@ -28,6 +29,10 @@ #include #include +#include +#include +#include + ScriptingConsole::ScriptingConsole(QWidget *parent) : ScriptingConsoleBase(parent) { connect(&ScriptSettings::instance(), &ScriptSettings::consoleSettingUpdate, @@ -135,6 +140,23 @@ void ScriptingConsole::applyScriptSettings() { auto &set = ScriptSettings::instance(); auto dfont = QFont(set.consoleFontFamily()); dfont.setPointSize(set.consoleFontSize()); + + auto thname = set.editorTheme(); + if (thname.isEmpty()) { + switch (SkinManager::instance().currentTheme()) { + case SkinManager::Theme::Dark: + setTheme(syntaxRepo().defaultTheme( + KSyntaxHighlighting::Repository::DarkTheme)); + break; + case SkinManager::Theme::Light: + setTheme(syntaxRepo().defaultTheme( + KSyntaxHighlighting::Repository::LightTheme)); + break; + } + } else { + setTheme(syntaxRepo().theme(thname)); + } + this->setFont(dfont); this->setTabWidth(set.consoleTabWidth()); this->setIndentationMode(WingCodeEdit::IndentationMode(set.consoleInden())); diff --git a/src/control/settingspopup.cpp b/src/control/settingspopup.cpp index 83e9f05..af069b6 100644 --- a/src/control/settingspopup.cpp +++ b/src/control/settingspopup.cpp @@ -37,18 +37,7 @@ #include "utilities.h" TreeFilterEdit::TreeFilterEdit(QWidget *parent) : QLineEdit(parent) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - const bool darkTheme = - (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); -#else - const bool darkTheme = - (palette().color(QPalette::Window).lightness() < 128); -#endif - - // TODO - // m_searchIcon = - //(QStringLiteral("search-filter"), - // darkTheme); + m_searchIcon = QIcon(QStringLiteral(":/qeditor/images/search.png")); setClearButtonEnabled(true); recomputeIconPos(); diff --git a/src/dialog/crashreport.cpp b/src/dialog/crashreport.cpp index 07471f8..f0d84da 100644 --- a/src/dialog/crashreport.cpp +++ b/src/dialog/crashreport.cpp @@ -1,3 +1,20 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + #include "crashreport.h" #include "ui_crashreport.h" diff --git a/src/dialog/definitiondownload.cpp b/src/dialog/definitiondownload.cpp new file mode 100644 index 0000000..aca8f75 --- /dev/null +++ b/src/dialog/definitiondownload.cpp @@ -0,0 +1,110 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + +#include "definitiondownload.h" + +#include +#include +#include +#include + +#include + +class ResizedPlainTextEdit : public QPlainTextEdit { +public: + ResizedPlainTextEdit(QWidget *parent) : QPlainTextEdit(parent) {} + +protected: + QSize sizeHint() const override { + const auto &metrics = fontMetrics(); + return QSize( + metrics.boundingRect(QString(60, QLatin1Char('x'))).width(), + metrics.height() * 10); + } +}; + +DefinitionDownloadDialog::DefinitionDownloadDialog( + KSyntaxHighlighting::Repository *repository, QWidget *parent) + : FramelessDialogBase(parent) { + setWindowTitle(tr("Update Syntax Definitions")); + setModal(true); + + auto w = new QWidget(this); + m_status = new ResizedPlainTextEdit(w); + m_status->setReadOnly(true); + + m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this); + m_buttonBox->button(QDialogButtonBox::Close)->setEnabled(false); + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + auto layout = new QVBoxLayout(w); + layout->setSpacing(10); + layout->addWidget(m_status); + layout->addWidget(m_buttonBox); + buildUpContent(w); + + m_downloader = + new KSyntaxHighlighting::DefinitionDownloader(repository, this); + QObject::connect( + m_downloader, + &KSyntaxHighlighting::DefinitionDownloader::informationMessage, + m_status, &QPlainTextEdit::appendPlainText); + QObject::connect(m_downloader, + &KSyntaxHighlighting::DefinitionDownloader::done, this, + &DefinitionDownloadDialog::downloadFinished); + + m_status->appendPlainText( + tr("Updating syntax definitions from online repository...")); + m_downloader->start(); + m_timer.start(); +} + +void DefinitionDownloadDialog::downloadFinished() { + qint64 elapsed = m_timer.elapsed(); + QString timeStr; + if (elapsed >= 60 * 60 * 1000) { + qint64 hours = elapsed / (60 * 60 * 1000); + elapsed %= (60 * 60 * 1000); + qint64 minutes = elapsed / (60 * 1000); + timeStr = + tr("%1:%2 hours").arg(hours).arg(minutes, 2, 10, QLatin1Char('0')); + } else if (elapsed >= 60 * 1000) { + qint64 minutes = elapsed / (60 * 1000); + elapsed %= (60 * 1000); + qint64 seconds = elapsed / 1000; + timeStr = tr("%1:%2 minutes") + .arg(minutes) + .arg(seconds, 2, 10, QLatin1Char('0')); + } else if (elapsed >= 1000) { + qint64 seconds = elapsed / 1000; + elapsed %= 1000; + timeStr = + tr("%1.%2 seconds").arg(seconds).arg((int)qRound(elapsed / 100.0)); + } else { + timeStr = tr("%1 ms").arg(elapsed); + } + m_status->appendPlainText( + tr("Update operation completed (%1)").arg(timeStr)); + m_buttonBox->button(QDialogButtonBox::Close)->setEnabled(true); + m_downloader->deleteLater(); +} + +void DefinitionDownloadDialog::closeEvent(QCloseEvent *e) { + if (!m_buttonBox->button(QDialogButtonBox::Close)->isEnabled()) + e->ignore(); +} diff --git a/src/dialog/definitiondownload.h b/src/dialog/definitiondownload.h new file mode 100644 index 0000000..758aa8d --- /dev/null +++ b/src/dialog/definitiondownload.h @@ -0,0 +1,53 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + +#ifndef QTEXTPAD_DEFINITIONDOWNLOAD_H +#define QTEXTPAD_DEFINITIONDOWNLOAD_H + +#include "dialog/framelessdialogbase.h" + +#include + +class QPlainTextEdit; +class QDialogButtonBox; + +namespace KSyntaxHighlighting { +class Repository; +class DefinitionDownloader; +} // namespace KSyntaxHighlighting + +class DefinitionDownloadDialog : public FramelessDialogBase { + Q_OBJECT + +public: + explicit DefinitionDownloadDialog( + KSyntaxHighlighting::Repository *repository, QWidget *parent); + +public Q_SLOTS: + void downloadFinished(); + +protected: + void closeEvent(QCloseEvent *) override; + +private: + KSyntaxHighlighting::DefinitionDownloader *m_downloader; + QPlainTextEdit *m_status; + QDialogButtonBox *m_buttonBox; + QElapsedTimer m_timer; +}; + +#endif // QTEXTPAD_DEFINITIONDOWNLOAD_H diff --git a/src/dialog/mainwindow.cpp b/src/dialog/mainwindow.cpp index 9cea2a3..96a29ac 100644 --- a/src/dialog/mainwindow.cpp +++ b/src/dialog/mainwindow.cpp @@ -2976,11 +2976,14 @@ void MainWindow::on_selectionChanged() { } void MainWindow::on_viewtxt() { - auto hexeditor = currentHexView(); - if (hexeditor == nullptr) { + auto editor = currentEditor(); + if (editor == nullptr) { return; } - _showtxt->load(hexeditor->document()->buffer()); + auto hexeditor = editor->hexEditor(); + QMimeDatabase db; + _showtxt->load(hexeditor->document()->buffer(), + db.mimeTypeForFile(editor->fileName()).name()); } void MainWindow::on_fullScreen() { diff --git a/src/dialog/searchdialog.cpp b/src/dialog/searchdialog.cpp new file mode 100644 index 0000000..4e532f3 --- /dev/null +++ b/src/dialog/searchdialog.cpp @@ -0,0 +1,618 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + +#include "searchdialog.h" +#include "utilities.h" + +#include "class/wingmessagebox.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +#include +#include +#endif + +static SearchDialog *s_instance = Q_NULLPTR; + +SearchWidget::SearchWidget(WingCodeEdit *editor, QWidget *parent) + : QWidget(parent), m_editor(editor) { + + auto tbMenu = new QToolButton(this); + tbMenu->setAutoRaise(true); + tbMenu->setIcon(QIcon::fromTheme(QStringLiteral("theme"))); + tbMenu->setToolTip(tr("Search Settings")); + tbMenu->setStyleSheet( + QStringLiteral("QToolButton::menu-indicator { image: none; }")); + + auto settingsMenu = new QMenu(this); + m_caseSensitive = settingsMenu->addAction(tr("Match ca&se")); + m_caseSensitive->setCheckable(true); + m_wholeWord = settingsMenu->addAction(tr("Match &whole words")); + m_wholeWord->setCheckable(true); + m_regex = settingsMenu->addAction(tr("Regular e&xpressions")); + m_regex->setCheckable(true); + m_escapes = settingsMenu->addAction(tr("&Escape sequences")); + m_escapes->setCheckable(true); + m_wrapSearch = settingsMenu->addAction(tr("Wrap Aro&und")); + m_wrapSearch->setCheckable(true); + tbMenu->setMenu(settingsMenu); + tbMenu->setPopupMode(QToolButton::InstantPopup); + + connect(m_caseSensitive, &QAction::triggered, this, + &SearchWidget::updateSettings); + connect(m_wholeWord, &QAction::triggered, this, + &SearchWidget::updateSettings); + connect(m_regex, &QAction::triggered, this, &SearchWidget::updateSettings); + connect(m_escapes, &QAction::triggered, this, + &SearchWidget::updateSettings); + connect(m_wrapSearch, &QAction::triggered, this, + &SearchWidget::updateSettings); + + m_searchText = new QLineEdit(this); + m_searchText->setClearButtonEnabled(true); + setFocusProxy(m_searchText); + + auto tbNext = new QToolButton(this); + tbNext->setAutoRaise(true); + tbNext->setIcon(ICONRES("")); + tbNext->setToolTip(tr("Find Next")); + + auto tbPrev = new QToolButton(this); + tbPrev->setAutoRaise(true); + tbPrev->setIcon(ICONRES("")); + tbPrev->setToolTip(tr("Find Previous")); + + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + layout->addWidget(tbMenu); + layout->addWidget(m_searchText); + layout->addWidget(tbNext); + layout->addWidget(tbPrev); + setLayout(layout); + + connect(m_searchText, &QLineEdit::textChanged, this, + [this](const QString &text) { + m_searchParams.searchText = + m_escapes->isChecked() + ? SearchDialog::translateEscapes(text) + : text; + m_editor->setLiveSearch(m_searchParams); + }); + connect(m_searchText, &QLineEdit::returnPressed, this, + [this] { searchNext(false); }); + connect(tbNext, &QToolButton::clicked, this, [this] { searchNext(false); }); + connect(tbPrev, &QToolButton::clicked, this, [this] { searchNext(true); }); +} + +void SearchWidget::setSearchText(const QString &text) { + m_searchText->setText(text); +} + +void SearchWidget::activate() { + m_searchParams.caseSensitive = m_caseSensitive->isChecked(); + m_searchParams.wholeWord = m_wholeWord->isChecked(); + m_searchParams.regex = m_regex->isChecked(); + + setFocus(Qt::OtherFocusReason); + m_searchText->selectAll(); + m_editor->setLiveSearch(m_searchParams); +} + +void SearchWidget::searchNext(bool reverse) { + if (!isVisible()) { + setVisible(true); + setEnabled(true); + activate(); + } + if (m_searchParams.searchText.isEmpty()) + return; + + auto searchCursor = m_editor->textSearch(m_editor->textCursor(), + m_searchParams, false, reverse); + if (searchCursor.isNull() && m_wrapSearch->isChecked()) { + QTextCursor wrapCursor = m_editor->textCursor(); + wrapCursor.movePosition(reverse ? QTextCursor::End + : QTextCursor::Start); + searchCursor = + m_editor->textSearch(wrapCursor, m_searchParams, true, reverse); + } + + if (searchCursor.isNull()) + QMessageBox::information(this, QString(), + tr("The specified text was not found")); + else + m_editor->setTextCursor(searchCursor); +} + +void SearchWidget::paintEvent(QPaintEvent *) { + QPainter painter(this); + + // Round the corners of this widget. Not strictly necessary, but + // it looks nicer... + const int arc = 4; + const int circ = 8; + const int h = height() - 1; + const int w = width() - 1; + painter.setPen(palette().color(QPalette::Mid)); + const QColor windowColor = palette().color(QPalette::Window); + painter.setBrush(windowColor); + painter.drawEllipse(0, h - circ, circ, circ); + painter.drawEllipse(w - circ, h - circ, circ, circ); + painter.fillRect(0, 0, arc, h - arc, windowColor); + painter.fillRect(w - arc, 0, arc, h - arc, windowColor); + painter.fillRect(arc, h - arc, w - circ, arc, windowColor); + painter.fillRect(arc, 0, w - circ, h - arc, windowColor); + painter.drawLine(0, 0, 0, h - arc); + painter.drawLine(arc, h, w - arc, h); + painter.drawLine(w, 0, w, h - arc); +} + +void SearchWidget::updateSettings() { + m_searchParams.caseSensitive = m_caseSensitive->isChecked(); + m_searchParams.wholeWord = m_wholeWord->isChecked(); + m_searchParams.regex = m_regex->isChecked(); + + m_editor->setLiveSearch(m_searchParams); +} + +/* Just sets some more sane defaults for QComboBox: + * - Don't auto-insert items (we handle that manually) + * - Disable the completer, since it insists on changing the typed + * item to match another item in the list that differs only in case. + */ +class SearchComboBox : public QComboBox { +public: + explicit SearchComboBox(QWidget *parent) : QComboBox(parent) { + setEditable(true); + setInsertPolicy(QComboBox::NoInsert); + setDuplicatesEnabled(true); + setCompleter(Q_NULLPTR); + } +}; + +SearchDialog::SearchDialog(QWidget *parent) + : FramelessDialogBase(parent), m_editor() { + s_instance = this; + setAttribute(Qt::WA_DeleteOnClose); + + setWindowTitle(tr("Find and Replace...")); + setWindowIcon(ICONRES("edit-find-replace")); + + m_searchText = new SearchComboBox(this); + m_replaceText = new SearchComboBox(this); + + m_caseSensitive = new QCheckBox(tr("Match ca&se"), this); + m_caseSensitive->setChecked(false); + m_wholeWord = new QCheckBox(tr("Match &whole words"), this); + m_wholeWord->setChecked(false); + m_regex = new QCheckBox(tr("Regular e&xpressions"), this); + m_regex->setChecked(false); + m_escapes = new QCheckBox(tr("&Escape sequences"), this); + m_escapes->setChecked(false); + m_wrapSearch = new QCheckBox(tr("Wrap Aro&und"), this); + m_wrapSearch->setChecked(false); + + // QDialogButtonBox insists on rearranging buttons depending on your + // platform, which would be fine if we only had standard actions, but most + // of our action buttons here are custom. + auto buttonBox = new QWidget(this); + buttonBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::MinimumExpanding); + auto buttonLayout = new QVBoxLayout(buttonBox); + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(5); + auto findNext = new QPushButton(tr("Find &Next"), this); + buttonLayout->addWidget(findNext); + auto findPrev = new QPushButton(tr("Find &Previous"), this); + buttonLayout->addWidget(findPrev); + auto replaceNext = new QPushButton(tr("&Replace"), this); + buttonLayout->addWidget(replaceNext); + auto replaceAll = new QPushButton(tr("Replace &All"), this); + buttonLayout->addWidget(replaceAll); + m_replaceSelectionButton = new QPushButton(tr("&In Selection"), this); + buttonLayout->addWidget(m_replaceSelectionButton); + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, + QSizePolicy::MinimumExpanding)); + auto closeButton = new QPushButton(tr("&Close"), this); + buttonLayout->addWidget(closeButton); + + connect(findNext, &QPushButton::clicked, this, + &SearchDialog::searchForward); + connect(findPrev, &QPushButton::clicked, this, + &SearchDialog::searchBackward); + connect(replaceNext, &QPushButton::clicked, this, + &SearchDialog::replaceCurrent); + connect(replaceAll, &QPushButton::clicked, this, + [this] { performReplaceAll(WholeDocument); }); + connect(m_replaceSelectionButton, &QPushButton::clicked, this, + [this] { performReplaceAll(InSelection); }); + connect(closeButton, &QPushButton::clicked, this, &QDialog::close); + + auto w = new QWidget(this); + auto layout = new QGridLayout(w); + layout->setContentsMargins(10, 10, 10, 10); + layout->setVerticalSpacing(5); + layout->setHorizontalSpacing(10); + auto searchLabel = new QLabel(tr("&Find:"), this); + searchLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + searchLabel->setBuddy(m_searchText); + layout->addWidget(searchLabel, 0, 0); + layout->addWidget(m_searchText, 0, 1, 1, 2); + auto replaceLabel = new QLabel(tr("Replace wit&h:"), this); + replaceLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + replaceLabel->setBuddy(m_replaceText); + layout->addWidget(replaceLabel, 1, 0); + layout->addWidget(m_replaceText, 1, 1, 1, 2); + layout->addItem(new QSpacerItem(0, 10, QSizePolicy::MinimumExpanding, + QSizePolicy::Fixed), + 2, 0, 1, 3); + layout->addWidget(m_caseSensitive, 3, 1); + layout->addWidget(m_wholeWord, 4, 1); + layout->addWidget(m_regex, 5, 1); + layout->addWidget(m_escapes, 6, 1); + layout->addWidget(m_wrapSearch, 3, 2); + layout->addItem( + new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding), + layout->rowCount(), 0, 1, 3); + layout->addWidget(buttonBox, 0, layout->columnCount(), layout->rowCount(), + 1); + buildUpContent(w); +} + +SearchDialog::~SearchDialog() { + syncSearchSettings(false); + s_instance = Q_NULLPTR; +} + +SearchDialog *SearchDialog::create(WingCodeEdit *editor, QWidget *parent) { + if (s_instance) { + s_instance->raise(); + } else { + Q_ASSERT(parent); + s_instance = new SearchDialog(parent); + s_instance->show(); + s_instance->raise(); + s_instance->activateWindow(); + + s_instance->m_editor = editor; + connect(s_instance->m_editor, &WingCodeEdit::selectionChanged, + s_instance, [] { + bool hasSelection = + s_instance->m_editor->textCursor().hasSelection(); + s_instance->m_replaceSelectionButton->setEnabled( + hasSelection); + }); + } + + const QTextCursor cursor = s_instance->m_editor->textCursor(); + if (cursor.hasSelection()) { + s_instance->m_searchText->setCurrentText(cursor.selectedText()); + s_instance->m_searchText->lineEdit()->selectAll(); + } + s_instance->m_replaceSelectionButton->setEnabled(cursor.hasSelection()); + + return s_instance; +} + +static QString translateCharEscape(QStringView digits, int *advance) { + Q_ASSERT(digits.size() > 0); + Q_ASSERT(advance); + + *advance = 0; + if (digits.at(0) == QLatin1Char('x')) { + // We only support exactly 2 hex bytes with \x format + if (digits.size() < 3) + return QString(); + QByteArray number = digits.mid(1, 2).toLatin1(); + char *end; + ulong ch = strtoul(number.constData(), &end, 16); + if (*end != '\0') + return QString(); + *advance = 2; + return QString(QChar::fromLatin1(static_cast(ch))); + } else if (digits.at(0) == QLatin1Char('u')) { + if (digits.size() < 5) + return QString(); + QByteArray number = digits.mid(1, 4).toLatin1(); + char *end; + ulong ch = strtoul(number.constData(), &end, 16); + if (*end != '\0') + return QString(); + *advance = 4; + return QString(QChar(static_cast(ch))); + } else if (digits.at(0) == QLatin1Char('U')) { + if (digits.size() < 9) + return QString(); + QByteArray number = digits.mid(1, 8).toLatin1(); + char *end; + ulong ch = strtoul(number.constData(), &end, 16); + if (*end != '\0') + return QString(); + + *advance = 8; + if (ch > 0xFFFFU) { + const QChar utf16[2] = { + QChar::highSurrogate(ch), + QChar::lowSurrogate(ch), + }; + return QString(utf16, 2); + } else { + return QString(QChar(static_cast(ch))); + } + } else { + // Octal no longer supported + qFatal("Unsupported character escape prefix"); + return QString(); + } +} + +QString SearchDialog::translateEscapes(const QString &text) { + QString result; + result.reserve(text.size()); + int start = 0; + for (;;) { + int pos = text.indexOf(QLatin1Char('\\'), start); + if (pos < 0 || pos + 1 >= text.size()) + break; + + result.append(text.sliced(start, pos - start)); + QChar next = text.at(pos + 1); + start = pos + 2; + switch (next.unicode()) { + case 'a': + result.append(QLatin1Char('\a')); + break; + case 'b': + result.append(QLatin1Char('\b')); + break; + case 'e': + result.append(QLatin1Char('\x1b')); + break; + case 'f': + result.append(QLatin1Char('\f')); + break; + case 'n': + result.append(QLatin1Char('\n')); + break; + case 'r': + result.append(QLatin1Char('\r')); + break; + case 't': + result.append(QLatin1Char('\t')); + break; + case 'v': + result.append(QLatin1Char('\v')); + break; + case '\\': + case '?': + case '\'': + case '"': + result.append(next); + break; + case 'x': // Hex byte + case 'u': // Unicode character (16-bit) + case 'U': // Unicode character (32-bit) + { + int advance; + const QString chars = + translateCharEscape(QStringView(text).mid(pos + 1), &advance); + if (chars.isEmpty()) { + // Translation failed + result.append(QLatin1Char('\\')); + result.append(next); + } else { + result.append(chars); + start += advance; + } + } break; + default: + // Just keep unrecognized sequences untranslated + result.append(QLatin1Char('\\')); + result.append(next); + break; + } + } + + result.append(text.sliced(start)); + return result; +} + +QString SearchDialog::regexReplace(const QString &text, + const QRegularExpressionMatch ®exMatch) { + QString result; + result.reserve(text.size()); + int start = 0; + for (;;) { + int pos = text.indexOf(QLatin1Char('\\'), start); + if (pos < 0 || pos + 1 >= text.size()) + break; + + result.append(text.sliced(start, pos - start)); + QChar next = text.at(pos + 1); + if (next.unicode() >= '0' && next.unicode() <= '9') { + // We support up to 99 replacements... + QByteArray number = QStringView(text).mid(pos + 1, 2).toLatin1(); + char *end; + ulong ref = strtoul(number.constData(), &end, 10); + result.append(regexMatch.captured(ref)); + start = pos + 1 + (end - number.constData()); + } else { + result.append(QLatin1Char('\\')); + result.append(next); + start = pos + 2; + } + } + + result.append(text.sliced(start)); + return result; +} + +void SearchDialog::syncSearchSettings(bool saveRecent) { + const QString searchText = m_searchText->currentText(); + if (!searchText.isEmpty() && saveRecent) { + if (m_searchText->count() == 0 || + m_searchText->itemText(0) != searchText) { + m_searchText->insertItem(0, searchText); + } + } + m_searchParams.searchText = + m_escapes->isChecked() ? translateEscapes(searchText) : searchText; + + const QString replaceText = m_replaceText->currentText(); + if (!replaceText.isEmpty() && saveRecent) { + if (m_replaceText->count() == 0 || + m_replaceText->itemText(0) != replaceText) { + m_replaceText->insertItem(0, replaceText); + } + } + + m_searchParams.caseSensitive = m_caseSensitive->isChecked(); + m_searchParams.wholeWord = m_wholeWord->isChecked(); + m_searchParams.regex = m_regex->isChecked(); +} + +QTextCursor SearchDialog::searchNext(bool reverse) { + Q_ASSERT(m_editor); + + if (m_searchParams.searchText.isEmpty()) + return QTextCursor(); + + auto searchCursor = m_editor->textSearch( + m_editor->textCursor(), m_searchParams, false, reverse, &m_regexMatch); + if (searchCursor.isNull() && m_wrapSearch->isChecked()) { + QTextCursor wrapCursor = m_editor->textCursor(); + wrapCursor.movePosition(reverse ? QTextCursor::End + : QTextCursor::Start); + searchCursor = m_editor->textSearch(wrapCursor, m_searchParams, true, + reverse, &m_regexMatch); + } + + if (searchCursor.isNull()) + WingMessageBox::information(this, QString(), + tr("The specified text was not found")); + else + m_editor->setTextCursor(searchCursor); + return searchCursor; +} + +void SearchDialog::searchForward() { + syncSearchSettings(true); + m_replaceCursor = searchNext(false); +} + +void SearchDialog::searchBackward() { + syncSearchSettings(true); + m_replaceCursor = searchNext(true); +} + +void SearchDialog::replaceCurrent() { + syncSearchSettings(true); + Q_ASSERT(m_editor); + + const QString searchText = m_searchText->currentText(); + if (searchText.isEmpty()) + return; + + if (m_replaceCursor.isNull() || m_replaceCursor != m_editor->textCursor()) { + m_replaceCursor = searchNext(false); + return; + } + + QString replaceText = m_replaceText->currentText(); + if (m_escapes->isChecked()) + replaceText = translateEscapes(replaceText); + + m_replaceCursor.beginEditBlock(); + m_replaceCursor.removeSelectedText(); + if (m_regex->isChecked()) + m_replaceCursor.insertText(regexReplace(replaceText, m_regexMatch)); + else + m_replaceCursor.insertText(replaceText); + m_replaceCursor.endEditBlock(); + + m_replaceCursor = searchNext(false); +} + +void SearchDialog::performReplaceAll(ReplaceAllMode mode) { + syncSearchSettings(true); + Q_ASSERT(m_editor); + + const QString searchText = m_searchText->currentText(); + if (searchText.isEmpty()) + return; + + auto searchCursor = m_editor->textCursor(); + if (mode == InSelection) + searchCursor.setPosition(m_editor->textCursor().selectionStart()); + else + searchCursor.movePosition(QTextCursor::Start); + searchCursor = m_editor->textSearch(searchCursor, m_searchParams, true, + false, &m_regexMatch); + if (searchCursor.isNull()) { + QMessageBox::information(this, QString(), + tr("The specified text was not found")); + return; + } else if (mode == InSelection && + searchCursor.selectionEnd() > + m_editor->textCursor().selectionEnd()) { + WingMessageBox::information( + this, QString(), + tr("The specified text was not found in the selection")); + return; + } + + QString replaceText = m_replaceText->currentText(); + if (m_escapes->isChecked()) + replaceText = translateEscapes(replaceText); + + searchCursor.beginEditBlock(); + auto replaceCursor = searchCursor; + int replacements = 0; + while (!replaceCursor.isNull()) { + if (mode == InSelection && replaceCursor.selectionEnd() > + m_editor->textCursor().selectionEnd()) + break; + + replaceCursor.removeSelectedText(); + if (m_regex->isChecked()) + replaceCursor.insertText(regexReplace(replaceText, m_regexMatch)); + else + replaceCursor.insertText(replaceText); + replaceCursor = m_editor->textSearch(replaceCursor, m_searchParams, + false, false, &m_regexMatch); + ++replacements; + } + searchCursor.endEditBlock(); + + WingMessageBox::information( + this, QString(), + tr("Successfully replaced %1 matches").arg(replacements)); +} diff --git a/src/dialog/searchdialog.h b/src/dialog/searchdialog.h new file mode 100644 index 0000000..c02c072 --- /dev/null +++ b/src/dialog/searchdialog.h @@ -0,0 +1,113 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + +#ifndef QTEXTPAD_SEARCHDIALOG_H +#define QTEXTPAD_SEARCHDIALOG_H + +#include +#include +#include + +#include "dialog/framelessdialogbase.h" +#include "wingcodeedit.h" + +class QLineEdit; +class QComboBox; +class QCheckBox; +class QPushButton; +class QTextCursor; + +class SearchWidget : public QWidget { + Q_OBJECT + +public: + explicit SearchWidget(WingCodeEdit *editor, QWidget *parent); + + QSize sizeHint() const override { + // Make the default just a bit wider + const QSize parentHint = QWidget::sizeHint(); + return {(parentHint.width() * 5) / 4, parentHint.height()}; + } + + void setSearchText(const QString &text); + void activate(); + +public Q_SLOTS: + void searchNext(bool reverse); + +protected: + void paintEvent(QPaintEvent *event) override; + +private Q_SLOTS: + void updateSettings(); + +private: + QLineEdit *m_searchText; + QAction *m_caseSensitive; + QAction *m_wholeWord; + QAction *m_regex; + QAction *m_escapes; + QAction *m_wrapSearch; + + WingCodeEdit *m_editor; + WingCodeEdit::SearchParams m_searchParams; +}; + +class SearchDialog : public FramelessDialogBase { + Q_OBJECT + +public: + static SearchDialog *create(WingCodeEdit *editor, QWidget *parent); + + virtual ~SearchDialog(); + + static QString translateEscapes(const QString &text); + static QString regexReplace(const QString &text, + const QRegularExpressionMatch ®exMatch); + +public Q_SLOTS: + QTextCursor searchNext(bool reverse); + +private Q_SLOTS: + void searchForward(); + void searchBackward(); + void replaceCurrent(); + +private: + explicit SearchDialog(QWidget *parent); + + void syncSearchSettings(bool saveRecent); + + enum ReplaceAllMode { WholeDocument, InSelection }; + void performReplaceAll(ReplaceAllMode mode); + + QComboBox *m_searchText; + QComboBox *m_replaceText; + QCheckBox *m_caseSensitive; + QCheckBox *m_wholeWord; + QCheckBox *m_regex; + QCheckBox *m_escapes; + QCheckBox *m_wrapSearch; + QPushButton *m_replaceSelectionButton; + QTextCursor m_replaceCursor; + + WingCodeEdit *m_editor; + WingCodeEdit::SearchParams m_searchParams; + QRegularExpressionMatch m_regexMatch; +}; + +#endif // QTEXTPAD_SEARCHDIALOG_H diff --git a/src/dialog/showtextdialog.cpp b/src/dialog/showtextdialog.cpp index 9314831..73453a3 100644 --- a/src/dialog/showtextdialog.cpp +++ b/src/dialog/showtextdialog.cpp @@ -23,11 +23,14 @@ #include "control/popupactionwidget.h" #include "control/settingspopup.h" #include "control/toast.h" +#include "dialog/definitiondownload.h" #include "dialog/encodingdialog.h" #include #include +#include + constexpr auto EMPTY_FUNC = [] {}; class SyntaxPopupAction : public PopupActionWidget { @@ -67,7 +70,6 @@ ShowTextDialog::ShowTextDialog(QWidget *parent) : FramelessDialogBase(parent) { layout->addWidget(q_check_ptr(m_ribbon)); m_edit = new CodeEdit(this); - m_edit->setShowLongLineEdge(false); m_edit->setReadOnly(true); m_edit->setAcceptDrops(false); @@ -79,6 +81,13 @@ ShowTextDialog::ShowTextDialog(QWidget *parent) : FramelessDialogBase(parent) { layout->addWidget(m_status); buildUpContent(cw); + m_syntaxButton = new QToolButton(this); + m_syntaxButton->setAutoRaise(true); + m_syntaxButton->setPopupMode(QToolButton::InstantPopup); + m_syntaxButton->addAction(new SyntaxPopupAction(this)); + + m_status->addPermanentWidget(m_syntaxButton); + // ok, preparing for starting... this->setWindowTitle(tr("TextBroswer")); this->setWindowIcon(ICONRES(QStringLiteral("viewtxt"))); @@ -91,38 +100,45 @@ ShowTextDialog::~ShowTextDialog() {} void ShowTextDialog::setSyntax(const KSyntaxHighlighting::Definition &syntax) { m_edit->setSyntax(syntax); + if (syntax.isValid()) { + m_syntaxButton->setText(syntax.translatedName() + QChar(' ') + + QChar(char16_t(0x25BC))); + } else { + m_syntaxButton->setText(tr("Plain Text") + QChar(' ') + + QChar(char16_t(0x25BC))); + } } -void ShowTextDialog::load(QHexBuffer *buffer, const QString encoding) { - // auto editor = m_edit->editor(); - // editor->blockSignals(true); - // editor->setCodec(encoding); - // load(buffer, offset, size); - // editor->blockSignals(false); -} - -void ShowTextDialog::load(QHexBuffer *buffer) { +void ShowTextDialog::load(QHexBuffer *buffer, const QString &mime, + const QString &encoding) { constexpr auto MAXLEN = 1024 * 1024 * 1024; if (buffer->length() > MAXLEN) { WingMessageBox::critical(this, this->windowTitle(), tr("TooHugeFile")); return; } - m_edit->setUpdatesEnabled(false); - - // auto doc = editor->document(); - // auto orign = offset; - - m_edit->setUpdatesEnabled(true); - // m_status->showMessage(editor->codecName()); show(); + m_status->showMessage(tr("Loading...")); + + QString enc; + auto ecs = Utilities::getEncodings(); + if (encoding.isEmpty() || !ecs.contains(encoding)) { + enc = QStringLiteral("ASCII"); + } else { + enc = encoding; + } + + this->buffer = buffer->read(qsizetype(0), buffer->length()); + m_edit->setPlainText(Utilities::decodingString(this->buffer, enc)); + m_status->showMessage(enc); + + setSyntax(WingCodeEdit::syntaxRepo().definitionForMimeType(mime)); } void ShowTextDialog::buildUpRibbonBar() { m_ribbon = new Ribbon(this); m_ribbon->setAcceptDrops(false); buildEditPage(m_ribbon->addTab(tr("Edit"))); - buildViewPage(m_ribbon->addTab(tr("View"))); } RibbonTabContent *ShowTextDialog::buildEditPage(RibbonTabContent *tab) { @@ -146,28 +162,20 @@ RibbonTabContent *ShowTextDialog::buildEditPage(RibbonTabContent *tab) { addPannelAction(pannel, QStringLiteral("encoding"), tr("Encoding"), &ShowTextDialog::on_encoding, shortcuts.keySequence(QKeySequences::Key::ENCODING)); + addPannelAction(pannel, QStringLiteral("defines"), tr("UpdateDefs"), + &ShowTextDialog::on_updateDefs); } return tab; } -RibbonTabContent *ShowTextDialog::buildViewPage(RibbonTabContent *tab) { - auto shortcuts = QKeySequences::instance(); - - auto pannel = tab->addGroup(tr("Display")); - - return tab; -} - void ShowTextDialog::on_copyfile() { m_edit->copy(); Toast::toast(this, NAMEICONRES(QStringLiteral("copy")), tr("CopyToClipBoard")); } -void ShowTextDialog::on_findfile() { - // m_edit->editor()->find(); -} +void ShowTextDialog::on_findfile() { m_edit->showSearchBar(true); } void ShowTextDialog::on_gotoline() { // m_edit->editor()->gotoLine(); @@ -177,8 +185,17 @@ void ShowTextDialog::on_encoding() { EncodingDialog d; if (d.exec()) { auto res = d.getResult(); - // m_edit->editor()->setCodec(res); + m_status->showMessage(tr("Loading...")); + m_edit->setPlainText(Utilities::decodingString(this->buffer, res)); + m_status->showMessage(res); } } -void ShowTextDialog::on_cancel() { m_canceled = true; } +void ShowTextDialog::on_updateDefs() { + auto downloadDialog = + new DefinitionDownloadDialog(&WingCodeEdit::syntaxRepo(), this); + downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + downloadDialog->show(); + downloadDialog->raise(); + downloadDialog->activateWindow(); +} diff --git a/src/dialog/showtextdialog.h b/src/dialog/showtextdialog.h index c2057c8..c036406 100644 --- a/src/dialog/showtextdialog.h +++ b/src/dialog/showtextdialog.h @@ -43,14 +43,13 @@ public: void setSyntax(const KSyntaxHighlighting::Definition &syntax); public: - void load(QHexBuffer *buffer, const QString encoding); - void load(QHexBuffer *buffer); + void load(QHexBuffer *buffer, const QString &mime, + const QString &encoding = {}); private: void buildUpRibbonBar(); RibbonTabContent *buildEditPage(RibbonTabContent *tab); - RibbonTabContent *buildViewPage(RibbonTabContent *tab); private: template @@ -119,21 +118,15 @@ private slots: void on_findfile(); void on_gotoline(); void on_encoding(); - - void on_cancel(); + void on_updateDefs(); private: Ribbon *m_ribbon = nullptr; CodeEdit *m_edit = nullptr; - bool m_canceled = false; - QStatusBar *m_status = nullptr; + QToolButton *m_syntaxButton = nullptr; - struct { - QHexBuffer *buffer = nullptr; - qsizetype offset = 0; - qsizetype size = -1; - } m_last; + QByteArray buffer; }; #endif // SHOWTEXTDIALOG_H diff --git a/src/settings/qeditconfig.cpp b/src/settings/qeditconfig.cpp index 90d82ed..0f453e5 100644 --- a/src/settings/qeditconfig.cpp +++ b/src/settings/qeditconfig.cpp @@ -1,17 +1,19 @@ -/**************************************************************************** +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer ** -** Copyright (C) 2006-2009 fullmetalcoder +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. ** -** This file is part of the Edyuk project +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. ** -** This file may be used under the terms of the GNU General Public License -** version 3 as published by the Free Software Foundation and appearing in the -** file GPL.txt included in the packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -****************************************************************************/ +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ #include "qeditconfig.h" diff --git a/src/settings/qeditconfig.h b/src/settings/qeditconfig.h index cef711c..e8e3fea 100644 --- a/src/settings/qeditconfig.h +++ b/src/settings/qeditconfig.h @@ -1,3 +1,20 @@ +/*============================================================================== +** Copyright (C) 2024-2027 WingSummer +** +** This program is free software: you can redistribute it and/or modify it under +** the terms of the GNU Affero General Public License as published by the Free +** Software Foundation, version 3. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +** details. +** +** You should have received a copy of the GNU Affero General Public License +** along with this program. If not, see . +** ============================================================================= +*/ + #ifndef QEDITCONFIG_H #define QEDITCONFIG_H