From bda20da10cfe73c2d9a03c5d855082ba25ddac9d Mon Sep 17 00:00:00 2001 From: Martchus Date: Wed, 22 Apr 2015 19:30:09 +0200 Subject: [PATCH] First commit --- android/AndroidManifest.xml | 48 + android/res/drawable-ldpi/icon.png | Bin 0 -> 3912 bytes android/res/values/strings.xml | 4 + cli/cli.cpp | 768 ++++++ cli/cli.h | 90 + deprecated/draganddroptablewidget.cpp | 81 + deprecated/draganddroptablewidget.h | 33 + general.pri | 71 + gui/initiate.h | 16 + gui/initiatequi.cpp | 42 + gui/mainwindow.cpp | 1171 +++++++++ gui/mainwindow.h | 129 + gui/mainwindow.ui | 516 ++++ gui/passwordgeneratordialog.cpp | 169 ++ gui/passwordgeneratordialog.h | 38 + gui/passwordgeneratordialog.ui | 349 +++ gui/stacksupport.cpp | 17 + gui/stacksupport.h | 104 + gui/undocommands.cpp | 458 ++++ gui/undocommands.h | 194 ++ main.cpp | 95 + model/entryfiltermodel.cpp | 32 + model/entryfiltermodel.h | 20 + model/entrymodel.cpp | 563 ++++ model/entrymodel.h | 127 + model/fieldmodel.cpp | 339 +++ model/fieldmodel.h | 121 + passwordmanager.doxygen | 2310 +++++++++++++++++ passwordmanager.pro | 116 + pkgbuild/default/PKGBUILD | 23 + pkgbuild/default/passwordmanager.install | 11 + pkgbuild/mingw-w64/PKGBUILD | 35 + qml/images/backarrow.png | Bin 0 -> 376 bytes qml/images/clear.png | Bin 0 -> 1783 bytes qml/images/close.png | Bin 0 -> 3328 bytes qml/images/magnifier.png | Bin 0 -> 3422 bytes qml/main.qml | 175 ++ qml/pages/AccountsPage.qml | 13 + qml/pages/BasicPage.qml | 182 ++ qml/pages/FieldsPage.qml | 14 + qml/pages/Separator.qml | 12 + qml/pages/StartPage.qml | 66 + qml/touch/ListViewDelegate.qml | 52 + qml/touch/TouchButton.qml | 43 + qml/touch/TouchLabel.qml | 35 + qml/touch/TouchScrollView.qml | 18 + qml/touch/TouchSlider.qml | 42 + qml/touch/TouchTextField.qml | 75 + quickgui/applicationinfo.cpp | 94 + quickgui/applicationinfo.h | 178 ++ quickgui/applicationpaths.h | 42 + quickgui/initiate.h | 16 + quickgui/initiatequick.cpp | 66 + .../applications/passwordmanager.desktop | 9 + resources/icons.qrc | 5 + .../hicolor/128x128/apps/passwordmanager.png | Bin 0 -> 3912 bytes .../hicolor/scalable/apps/passwordmanager.svg | 1 + resources/qml.qrc | 20 + resources/windowsicon.rc | 1 + translations/passwordmanager_de_DE.qm | Bin 0 -> 23 bytes translations/passwordmanager_de_DE.ts | 620 +++++ translations/passwordmanager_en_US.qm | Bin 0 -> 703 bytes translations/passwordmanager_en_US.ts | 620 +++++ util/testroutines.cpp | 37 + util/testroutines.h | 10 + 65 files changed, 10536 insertions(+) create mode 100644 android/AndroidManifest.xml create mode 100644 android/res/drawable-ldpi/icon.png create mode 100644 android/res/values/strings.xml create mode 100644 cli/cli.cpp create mode 100644 cli/cli.h create mode 100644 deprecated/draganddroptablewidget.cpp create mode 100644 deprecated/draganddroptablewidget.h create mode 100644 general.pri create mode 100644 gui/initiate.h create mode 100644 gui/initiatequi.cpp create mode 100644 gui/mainwindow.cpp create mode 100644 gui/mainwindow.h create mode 100644 gui/mainwindow.ui create mode 100644 gui/passwordgeneratordialog.cpp create mode 100644 gui/passwordgeneratordialog.h create mode 100644 gui/passwordgeneratordialog.ui create mode 100644 gui/stacksupport.cpp create mode 100644 gui/stacksupport.h create mode 100644 gui/undocommands.cpp create mode 100644 gui/undocommands.h create mode 100644 main.cpp create mode 100644 model/entryfiltermodel.cpp create mode 100644 model/entryfiltermodel.h create mode 100644 model/entrymodel.cpp create mode 100644 model/entrymodel.h create mode 100644 model/fieldmodel.cpp create mode 100644 model/fieldmodel.h create mode 100644 passwordmanager.doxygen create mode 100644 passwordmanager.pro create mode 100644 pkgbuild/default/PKGBUILD create mode 100644 pkgbuild/default/passwordmanager.install create mode 100644 pkgbuild/mingw-w64/PKGBUILD create mode 100644 qml/images/backarrow.png create mode 100644 qml/images/clear.png create mode 100644 qml/images/close.png create mode 100644 qml/images/magnifier.png create mode 100644 qml/main.qml create mode 100644 qml/pages/AccountsPage.qml create mode 100644 qml/pages/BasicPage.qml create mode 100644 qml/pages/FieldsPage.qml create mode 100644 qml/pages/Separator.qml create mode 100644 qml/pages/StartPage.qml create mode 100644 qml/touch/ListViewDelegate.qml create mode 100644 qml/touch/TouchButton.qml create mode 100644 qml/touch/TouchLabel.qml create mode 100644 qml/touch/TouchScrollView.qml create mode 100644 qml/touch/TouchSlider.qml create mode 100644 qml/touch/TouchTextField.qml create mode 100644 quickgui/applicationinfo.cpp create mode 100644 quickgui/applicationinfo.h create mode 100644 quickgui/applicationpaths.h create mode 100644 quickgui/initiate.h create mode 100644 quickgui/initiatequick.cpp create mode 100644 resources/desktop/applications/passwordmanager.desktop create mode 100644 resources/icons.qrc create mode 100644 resources/icons/hicolor/128x128/apps/passwordmanager.png create mode 100644 resources/icons/hicolor/scalable/apps/passwordmanager.svg create mode 100644 resources/qml.qrc create mode 100644 resources/windowsicon.rc create mode 100644 translations/passwordmanager_de_DE.qm create mode 100644 translations/passwordmanager_de_DE.ts create mode 100644 translations/passwordmanager_en_US.qm create mode 100644 translations/passwordmanager_en_US.ts create mode 100644 util/testroutines.cpp create mode 100644 util/testroutines.h diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 0000000..03581fd --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/res/drawable-ldpi/icon.png b/android/res/drawable-ldpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2d2a6348c11483d5e7d9101f7ae2977b970bbbd5 GIT binary patch literal 3912 zcmZWscQD*-*Z%FY?CMsJh~5?!^005v=d7_BE&bWUAN^nI_a=L#51f*v&0|3=u6-7B+@663CTkOqFkIt*o zmrPL|mgNsr%MdTmB0G&x)Z+h|KO80M=6_DUtsgjVEB76Lh7!#qC2@-BX#gt#YayN3mAZD zbG52Gd8i3q?Dc*fVMdqFJX2*)2p#KxA7A_w=v7x%{*Dw#-A;J#3t7ldGDzgd|DMC| z5!nB>*e6DevuWCg6h8(O6cjemp-3S)LMZ<2azJ~0t`VQGZEHLu_KqNIAe2PI#%2)A zq5Y?-iw<`bDooP+s{fIZj$Jm!qtF*bK~3$I_yCK8wTR+IqdHko<+_R``ik13xdrQD zB16f2I7XD44qaO=Ojt zaMWtYVx1-Ek1^wf{H^4~l4&(M@RtSXB-fV+2{Xo!&}h1~g&FCy?1HhP+}n_y_EqKk zwqVWR0a75IH$F`mJ|RIdq~)`nj{-J-F+b$`)$=SYk$d38%X52tw?);t1XoXoujCfJ z3^xzBNkc34;$B9p0|GRZH|@lvF*GnT8WS@r5CKJVDTFq$Z*3E{iXK+T|9uR+PF1?< zii!nW$^uJj5cQwE0{y!^v}7dZ)s=)Fo^1=%o8y;SO}a1-PTZ$Czsf@KxJzlUzxv%5 z5AQ@N-rKA)+Fy&Pw$Sk9kAU`i*EjK=6n-(P@hN`FI@}1{=DC!GubIw>Vfm0@@wvX6 zdIsqk>18Fpl)*t99J?Xz4(|rU$O0K3{`AE5>6CD{wgz4Gw9Xw2!iNcG9($k7yL+$7!FR)|1y55 zFpM{%ocOQ9#b&-i63bh6*cl?p4HO<>9^FinI0B`o)6IGOrW8iY^bMk z*McjySvag2Bp@JFZ>!K`!_hztW$dGOB1^ZjQjyMt8$A{8B07aed*J6qG?sJomlvhF zi$l5G%GD|>oe&$fx_x=5yStb4U)k}pl^crkeFU%Lxx*?uKi7=(0`Q7q=cpuy#$B;-hMJz;8C5r*9eTl88! z9Ng8>S#>Z==)t_}u@=f)4hf-_!H%=qXq!f^ zw5;E;iD=!5dD1lv0B>2rxc5tFED7--#hceWe-q*7gMyUt4|KPTkr^HbhjG0hL2}1+ zE7N2JFc7nzt>(R)nhSz4pHA-hvUA`cJpTFgo;_y>DM>T&e4&z;)#KX<<&OM1z3mkT zkDSi*M6$`vZEPI;OWx0!*W9X7r}IfGC5AO-@trj`%qDRTok{$hUe5;(NOo z)$X&iv6-yb2U6`C19v$H6=9j1bE|H3N_A#nwEjVc87s3u?fQ%*3F+~2SomCcVw8kY z*F<291CsjguH5siVsJmiTxBv{bmD4Dbc|mU9w_OJy()G(tD+AfB6^`7RUB;D+I+d! z0ah0b9DUYVgm(kct4?fslKQF7=pRh*2XVC`rXJ|P3`W^eKVETbpjA`vy`bk6cW){) ziFBjdF$L-kJQ+17<`|h)#aj2eTS?i!UR|ugL#Q7~faC%jcD%dg1m6uEYLq;D2FWJJ zi>b1C2HGcY2VS`yH@ZadGR_!{ZBom!*3T{%$v&HJ0)!_T&r4Ut`jm7U0%W}B@flk_ zn4H!Sf|vEQ$s@XwP1h=LF%9%siTW9VNzif+n|W(70aq$&xJ{!on4@ zmqpCrl~c;N1}sl-_0S7k41^w;stoV}o@jH6`No^olS8H(}U=U=X2E+tc0@ zUdF!Dj$SFt4bJ{IUW&6avN_ww=1NUhY#jDR|8a9`XP#Nu-xX;Rl`TJ=9J{LZC2SNa zQIx7O;1!@eucvMe?yVHM7cR11?HoBDUU|DJPfbjFe4N^NDESuM;kG{Y-DGc=h4sbq zr!$s&Io-9E(;TMyQx{I>)uJRCix@i|Ph8ITq60XB3FT8zCCw83NK?@)h{my83vSQ- zQZ%cSe#(n?utG~XF_vYGqVw03&v(hdEUf*pCd$gtufwl(l)l2x>9VpVu+JV)l*Y9e z+E2yszJ5_n`lCpx5P&O`d$Q2Kw6l zK_2V;*N1eh%FI+-B;zS}!c);7lHnE{cf^UB+9W<7cU+QYG^Av9=VfM%u_-;Mt$kFq z&dc-irNxbXWwAUAW>63_lp=dmqGE^qK9VR1k{3>66db-Q!W(0TGT;_-k@3ti_iGtE zZKQ4Wnmsr9SOO*G;35u@qk-us8#tXD!%Y@!rvsdvTCZhB>v9 zrh%f!ZQjLO-1y!YAA~>2%eaJGYgrq|OwvuIod-ems$#H_OufFPwDB-87zcFUaF$8#bBhnv)f1YKWAa1yObg=0y zS_jjqt4eW23uwt}_DD?+?A~@-f5odj z%tAS@Pb2B#ArwLpIO~#vVJ^9hnZ0dQdKl`f=qp380K7}SBPfB-*w5fl{Hq}qB?To@ zi{%d)a?odx%}U;WbZ9&hBndVz6nXQ;9Cw#k6^bW>`)bHBU1C#%s7PvDrM?KL6pye_ zAVBVVIMu_fjHEVT8P-KdW4$9u5jq?zB(zPIFDw^rAMjfmM@H;|?uE(Uv4d!F46tZF z2f-ynh{ZTjN>yB1{D9Q+4ML>ZBc5CtB0)N!4_=VZq*_WxfwLZ7*5OSykk|U)^r6r0 zmMYj>a;{w!ts=*?E~B73AF}5+x;{d}q~9(5Ob3$M=!eRTO(84VOVdJDWb`=Z_y%?|%-7UtEbTXm-GpLc$d}%|Z(5DI>?afNPPzSsb zf6FGfRRx?Q{v+dm$=)zV8rH5kS~cFHE@jmhn80YMz&JjZwXiqFia?yL((oB zHtz|;5xodnfW_T3Z07NIL`A$;Q9f;#@vA_loFWb`-batKkBQILiT*R~{1DXGu}%74 zy^@XqsgcFWI?;IOh%j@pb&{CY&n<+-WR;NOTOh(z z*PVCDDFl!RQfxQE%~-nB^SCRNq}z;jHhxD9*t0)aC3z#cG#9RXUi9U~S50 zOF@?=z&%U|zNCcK_igz>V&e@C{ zxxOH=Y92Y&7CS2ccAIlxTql^3gV!DIO%;E6_iQ5VWmot2HX(dM-}%YzY;t4%oL$9; zn()SUO#nk1j~Zx@ul?7K(eF!Gpyo67{}0HyEJ|?&@^>4^x;}*g6{V+&CGzIM{{x=n BM_>Q| literal 0 HcmV?d00001 diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml new file mode 100644 index 0000000..77147bb --- /dev/null +++ b/android/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Password Manager + diff --git a/cli/cli.cpp b/cli/cli.cpp new file mode 100644 index 0000000..023e475 --- /dev/null +++ b/cli/cli.cpp @@ -0,0 +1,768 @@ +#include "cli.h" + +#include +#include +#include +#include +#include + +#include + +#include + +#if defined(PLATFORM_UNIX) +#include +#include +#endif + +#include +#include + +using namespace std; +using namespace std::placeholders; +using namespace ConversionUtilities; +using namespace Io; + +namespace Cli { + +InputMuter::InputMuter() +{ +#if defined(PLATFORM_UNIX) + tcgetattr(STDIN_FILENO, &m_attr); + termios newAttr = m_attr; + newAttr.c_lflag &= ~ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &newAttr); +#elif defined(PLATFORM_WINDOWS) + m_cinHandle = GetStdHandle(STD_INPUT_HANDLE); + m_mode = 0; + GetConsoleMode(m_cinHandle, &m_mode); + SetConsoleMode(m_cinHandle, m_mode & (~ENABLE_ECHO_INPUT)); +#endif +} + +InputMuter::~InputMuter() +{ +#if defined(PLATFORM_UNIX) + tcsetattr(STDIN_FILENO, TCSANOW, &m_attr); +#elif defined(PLATFORM_WINDOWS) + SetConsoleMode(m_cinHandle, m_mode); +#endif +} + +void clearConsole() +{ +#if defined(PLATFORM_WINDOWS) + HANDLE hStdOut; + CONSOLE_SCREEN_BUFFER_INFO csbi; + DWORD count; + DWORD cellCount; + COORD homeCoords = {0, 0}; + hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + if(hStdOut == INVALID_HANDLE_VALUE) { + return; + } + // get the number of cells in the current buffer + if(!GetConsoleScreenBufferInfo(hStdOut, &csbi)) { + return; + } + cellCount = csbi.dwSize.X * csbi.dwSize.Y; + // fill the entire buffer with spaces + if(!FillConsoleOutputCharacter(hStdOut, (TCHAR) ' ', cellCount, homeCoords, &count)) { + return; + } + // fill the entire buffer with the current colors and attributes + if(!FillConsoleOutputAttribute(hStdOut, csbi.wAttributes, cellCount, homeCoords, &count)) { + return; + } + // move the cursor home + SetConsoleCursorPosition(hStdOut, homeCoords); +#else + EscapeCodes::setCursor(cout); + EscapeCodes::eraseDisplay(cout); +#endif +} + +InteractiveCli::InteractiveCli() : + m_o(cout), + m_i(cin), + m_currentEntry(nullptr), + m_modified(false), + m_quit(false) +{} + +void InteractiveCli::run(const string &file) +{ + if(!file.empty()) { + openFile(file, false); + } + string input; + while(!m_quit) { + getline(m_i, input); + if(!input.empty()) { + processCommand(input); + } + } +} + +void InteractiveCli::processCommand(const string &cmd) +{ +#define CMD(value) !paramMissing && cmd == value +#define CMD2(value1, value2) !paramMissing && (cmd == value1 || cmd == value2) +#define CMD_P(value) !paramMissing && checkCommand(cmd, value, param, paramMissing) +#define CMD2_P(value1, value2) !paramMissing && (checkCommand(cmd, value1, param, paramMissing) || checkCommand(cmd, value2, param, paramMissing)) + + string param; + bool paramMissing = false; + if(CMD2("quit", "q")) { + quit(); + } else if(CMD("q!")) { + m_quit = true; + } else if(CMD("wq")) { + saveFile(); + m_quit = true; + } else if(CMD2("clear", "c")) { + clearConsole(); + } else if(CMD2_P("openreadonly", "or")) { + openFile(param, true); + } else if(CMD2_P("open", "o")) { + openFile(param, false); + } else if(CMD2("close", "c")) { + closeFile(); + } else if(CMD2("save", "w")) { + saveFile(); + } else if(CMD2_P("create", "cr")) { + createFile(param); + } else if(CMD("chpassphrase")) { + changePassphrase(); + } else if(CMD("rmpassphrase")) { + removePassphrase(); + } else if(CMD("pwd")) { + pwd(); + } else if(CMD_P("cd")) { + cd(param); + } else if(CMD("ls")) { + ls(); + } else if(CMD2("tree", "t")) { + tree(); + } else if(CMD2_P("mknode", "mkn")) { + makeEntry(EntryType::Node, param); + } else if(CMD2_P("mkaccount", "mka")) { + makeEntry(EntryType::Account, param); + } else if(CMD2("rmentry", "rme")) { + removeEntry("."); + } else if(CMD2_P("rmentry", "rme")) { + removeEntry(param); + } else if(CMD2_P("rnentry", "rne")) { + renameEntry(param); + } else if(CMD2_P("mventry", "me")) { + moveEntry(param); + } else if(CMD2_P("readfield", "rf")) { + readField(param); + } else if(CMD2_P("setfield", "sf")) { + setField(false, param); + } else if(CMD2_P("setfieldpw", "sp")) { + setField(true, param); + } else if(CMD2_P("rmfield", "rf")) { + removeField(param); + } else if(CMD2("help", "?")) { + printHelp(); + } else if(paramMissing) { + m_o << "parameter is missing" << endl; + } else { + m_o << "command is unknown" << endl; + } +} + +Entry *InteractiveCli::resolvePath(const string &path) +{ + auto parts = splitString >(path, "/", EmptyPartsTreat::Merge); + bool fromRoot = path.at(0) == '/'; + if(fromRoot && parts.empty()) { + return m_file.rootEntry(); + } else { + Entry *entry = fromRoot ? m_file.rootEntry() : m_currentEntry; + for(const string &part : parts) { + if(part == "..") { + if(entry->parent()) { + entry = entry->parent(); + } else { + m_o << "can not resolve path; entry \"" << entry->label() << "\" is root" << endl; + return nullptr; + } + } else if(part != ".") { + switch(entry->type()) { + case EntryType::Account: + m_o << "can not resolve path; entry \"" << entry->label() << "\" is not a node entry" << endl; + return nullptr; + case EntryType::Node: + for(Entry *child : (static_cast(entry)->children())) { + if(child->label() == part) { + entry = child; + goto next; + } + } + m_o << "can not resolve path; entry \"" << entry->label() << "\" has no child \"" << part << "\"" << endl; + return nullptr; + } + } + next:; + } + return entry; + } +} + +bool InteractiveCli::checkCommand(const string &str, const char *phrase, std::string ¶m, bool ¶mMissing) +{ + for(auto i = str.cbegin(), end = str.cend(); i != end; ++i, ++phrase) { + if(*phrase == 0) { + if(*i == ' ') { + if(++i != end) { + param.assign(i, end); + return true; + } else { + paramMissing = true; + } + } + return false; + } else if(*i != *phrase) { + return false; + } + } + paramMissing = *phrase == 0; + return false; +} + +void InteractiveCli::openFile(const string &file, bool readOnly) +{ + if(m_file.isOpen()) { + m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl; + } else { + m_file.setPath(file); + try { + try { + m_file.open(readOnly); + if(m_file.isEncryptionUsed()) { + m_file.setPassword(askForPassphrase()); + } + m_file.load(); + m_currentEntry = m_file.rootEntry(); + m_o << "file \"" << file << "\" opened" << endl; + } catch (ParsingException &) { + m_o << "error occured when parsing file \"" << file << "\"" << endl; + throw; + } catch (CryptoException &) { + m_o << "error occured when decrypting file \"" << file << "\"" << endl; + throw; + } catch (ios_base::failure &) { + m_o << "IO error occured when opening file \"" << file << "\"" << endl; + throw; + } catch (exception &) { + m_o << "an unexpected exception occured when opening file \"" << file << "\"" << endl; + terminate(); + } + } catch(exception &e) { + if(*e.what() != 0) { + m_o << e.what() << endl; + } + m_file.clear(); + m_currentEntry = nullptr; + } + m_modified = false; + } +} + +void InteractiveCli::closeFile() +{ + if(m_file.isOpen()) { + m_file.clear(); + m_currentEntry = nullptr; + m_o << "file closed" << endl; + } else { + m_o << "no file was opened" << endl; + } +} + +void InteractiveCli::saveFile() +{ + if(m_file.isOpen()) { + try { + try { + m_file.save(*m_file.password()); + m_o << "file \"" << m_file.path() << "\" saved" << endl; + } catch (ParsingException &) { + m_o << "error occured when parsing file \"" << m_file.path() << "\"" << endl; + throw; + } catch (CryptoException &) { + m_o << "error occured when encrypting file \"" << m_file.path() << "\"" << endl; + throw; + } catch (ios_base::failure &) { + m_o << "IO error occured when saving file \"" << m_file.path() << "\"" << endl; + throw; + } catch (exception &) { + m_o << "an unexpected exception occured when saving file \"" << m_file.path() << "\"" << endl; + terminate(); + } + } catch(exception &e) { + if(*e.what() != 0) { + m_o << e.what() << endl; + } + m_o << "file has been closed; try reopening the file" << endl; + m_file.clear(); + } + m_modified = false; + } else { + m_o << "nothing to save; no file opened or created" << endl; + } +} + +void InteractiveCli::createFile(const string &file) +{ + if(m_file.isOpen()) { + m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl; + } else { + m_file.setPath(file); + try { + try { + m_file.create(); + m_file.generateRootEntry(); + m_currentEntry = m_file.rootEntry(); + m_o << "file \"" << file << "\" created and opened" << endl; + } catch (ios_base::failure &) { + m_o << "IO error occured when creating file \"" << file << "\"" << endl; + throw; + } catch (exception &) { + m_o << "an unexpected exception occured when creating file \"" << file << "\"" << endl; + terminate(); + } + } catch(exception &e) { + if(*e.what() != 0) { + m_o << e.what() << endl; + } + m_file.clear(); + m_currentEntry = nullptr; + } + m_modified = false; + } +} + +void InteractiveCli::changePassphrase() +{ + if(m_file.isOpen()) { + try { + m_file.setPassword(askForPassphrase(true)); + m_modified = true; + m_o << "passphrase changed; use save to apply" << endl; + } catch(runtime_error &) { + m_o << "passphrase has not changed" << endl; + } + } else { + m_o << "can not set passphrase; no file opened or created" << endl; + } +} + +void InteractiveCli::removePassphrase() +{ + if(m_file.isOpen()) { + if(*m_file.password()) { + m_file.clearPassword(); + m_o << "passphrase removed; use save to apply" << endl; + m_modified = true; + } else { + m_o << "nothing to remove; no passphrase present on current file" << endl; + } + } else { + m_o << "nothing to remove; no file opened or created" << endl; + } +} + +void InteractiveCli::pwd() +{ + if(m_file.isOpen()) { + auto path = m_currentEntry->path(); + m_o << path.front() << ": /"; + path.pop_front(); + m_o << joinStrings(path, "/") << endl; + } else { + m_o << "no file open" << endl; + } +} + +void InteractiveCli::cd(const string &path) +{ + if(m_file.isOpen()) { + if(Entry *entry = resolvePath(path)) { + m_currentEntry = entry; + m_o << "changed to \"" << entry->label() << "\"" << endl; + } + } else { + m_o << "can not change directory; no file open" << endl; + } +} + +void InteractiveCli::ls() +{ + if(m_file.isOpen()) { + switch(m_currentEntry->type()) { + case EntryType::Account: { + m_o << "fields:"; + for(const Field &field : static_cast(m_currentEntry)->fields()) { + m_o << " " << field.name(); + } + break; + } case EntryType::Node: { + m_o << "entries:"; + for(const Entry *entry : static_cast(m_currentEntry)->children()) { + m_o << " " << entry->label(); + } + break; + } + } + m_o << endl; + } else { + m_o << "can not list any entires; no file open" << endl; + } +} + +void InteractiveCli::tree() +{ + if(m_file.isOpen()) { + function printEntries; + printEntries = [&printEntries, this] (const Entry *entry, unsigned char level) { + for(unsigned char i = 0; i < level; ++i) { + m_o << " "; + } + m_o << entry->label() << endl; + if(entry->type() == EntryType::Node) { + for(const Entry *child : (static_cast(entry)->children())) { + printEntries(child, level + 2); + } + } + }; + printEntries(m_currentEntry, 0); + } else { + m_o << "can not print tree; no file open" << endl; + } +} + +void InteractiveCli::makeEntry(EntryType entryType, const string &label) +{ + if(m_file.isOpen()) { + switch(m_currentEntry->type()) { + case EntryType::Node: + switch(entryType) { + case EntryType::Node: + m_o << "node entry \"" << (new NodeEntry(label, static_cast(m_currentEntry)))->label() << "\" created" << endl; + break; + case EntryType::Account: + m_o << "account entry \"" << (new AccountEntry(label, static_cast(m_currentEntry)))->label() << "\" created" << endl; + break; + } + m_modified = true; + break; + case EntryType::Account: + m_o << "can not make entry; current entry is no node entry" << endl; + } + } else { + m_o << "can not make entry; no file open" << endl; + } +} + +void InteractiveCli::removeEntry(const string &path) +{ + if(m_file.isOpen()) { + if(Entry *entry = resolvePath(path)) { + if(entry == m_file.rootEntry()) { + m_o << "can not remove root entry" << endl; + } else { + if(entry == m_currentEntry) { + m_currentEntry = entry->parent(); + } + m_o << "removed entry \"" << entry->label() << "\"" << endl; + delete entry; + m_modified = true; + } + } + } else { + m_o << "can not remove entry; no file open" << endl; + } +} + +void InteractiveCli::renameEntry(const string &path) +{ + if(m_file.isOpen()) { + if(Entry *entry = resolvePath(path)) { + string label; + m_o << "enter new name: " << endl; + getline(m_i, label); + if(label.empty()) { + m_o << "can not rename; new name is empty" << endl; + } else { + entry->setLabel(label); + m_o << "entry renamed to \"" << entry->label() << "\"" << endl; + m_modified = true; + } + } + } else { + m_o << "can not rename entry; no file open" << endl; + } +} + +void InteractiveCli::moveEntry(const string &path) +{ + if(m_file.isOpen()) { + if(Entry *entry = resolvePath(path)) { + string newParentPath; + m_o << "enter path of new parent: " << endl; + getline(m_i, newParentPath); + if(newParentPath.empty()) { + m_o << "can not move; path of new parent is empty" << endl; + } else { + if(Entry *newParent = resolvePath(newParentPath)) { + switch(newParent->type()) { + case EntryType::Account: + m_o << "can not move; new parent must be a node entry" << endl; + break; + case EntryType::Node: + if(entry->parent() == entry) { + m_o << "element not moved; parent doesn't change" << endl; + } else if(entry->type() == EntryType::Node && newParent->isIndirectChildOf(static_cast(entry))) { + m_o << "can not move; new parent mustn't be child of the entry to move" << endl; + } else { + entry->setParent(static_cast(newParent)); + m_o << "entry moved to \"" << newParent->label() << "\"" << endl; + m_modified = true; + } + } + } + } + } + } else { + m_o << "can not rename entry; no file open" << endl; + } +} + +void InteractiveCli::readField(const string &fieldName) +{ + if(m_file.isOpen()) { + if(m_currentEntry->type() == EntryType::Account) { + const vector &fields = static_cast(m_currentEntry)->fields(); + bool valuesFound = false; + for(const Field &field : fields) { + if(field.name() == fieldName) { + m_o << field.value() << endl; + valuesFound = true; + } + } + if(!valuesFound) { + m_o << "field \"" << fieldName << "\" does not exist" << endl; + } + } else { + m_o << "can not read field; current entry is no account entry" << endl; + } + } else { + m_o << "can not read field; no file open" << endl; + } +} + +void InteractiveCli::setField(bool useMuter, const string &fieldName) +{ + if(m_file.isOpen()) { + if(m_currentEntry->type() == EntryType::Account) { + vector &fields = static_cast(m_currentEntry)->fields(); + unsigned int valuesFound = 0; + string value; + m_o << "enter new value: "; + if(useMuter) { + InputMuter m; + getline(m_i, value); + m_o << endl << "repeat: "; + string repeat; + getline(m_i, repeat); + if(value != repeat) { + m_o << "values do not match; field has not been altered" << endl; + return; + } + } else { + getline(m_i, value); + } + for(Field &field : fields) { + if(field.name() == fieldName) { + ++valuesFound; + if(valuesFound == 1) { + field.setValue(value); + } else { + m_o << "enter new value for " << valuesFound << ". field (with the specified field): "; + value.clear(); + if(useMuter) { + InputMuter m; + getline(m_i, value); + m_o << endl << "repeat: "; + string repeat; + getline(m_i, repeat); + if(value == repeat) { + field.setValue(value); + } else { + m_o << "values do not match; field has not been altered" << endl; + } + } else { + getline(m_i, value); + field.setValue(value); + } + } + field.setType(useMuter ? FieldType::Password : FieldType::Normal); + } + } + switch(valuesFound) { + case 0: + fields.emplace_back(static_cast(m_currentEntry), fieldName, value); + if(useMuter) { + fields.back().setType(FieldType::Password); + } + m_o << "new field with value inserted" << endl; + break; + case 1: + m_o << "value updated" << endl; + m_modified = true; + break; + default: + m_o << valuesFound << " values updated" << endl; + m_modified = true; + } + } else { + m_o << "can not set field; current entry is no account entry" << endl; + } + } else { + m_o << "can not set field; no file open" << endl; + } +} + +void InteractiveCli::removeField(const string &fieldName) +{ + if(m_file.isOpen()) { + if(m_currentEntry->type() == EntryType::Account) { + vector &fields = static_cast(m_currentEntry)->fields(); + unsigned int valuesFound = 0; + for(Field &field : fields) { + if(field.name() == fieldName) { + ++valuesFound; + } + } + switch(valuesFound) { + valuesFound = 0; + case 0: + break; + case 1: + fields.erase(remove_if(fields.begin(), fields.end(), [&fieldName] (Field &field) { + return field.name() == fieldName; + })); + break; + default: + fields.erase(remove_if(fields.begin(), fields.end(), [this, &fieldName, &valuesFound] (Field &field) { + if(field.name() == fieldName) { + m_o << "remove " << ++valuesFound << ". occurrence? [y]=yes, different key=no " << endl; + string res; + getline(m_i, res); + return !(res == "y" || res == "yes"); + } else { + return false; + } + })); + } + switch(valuesFound) { + case 0: + m_o << "can not remove field; specified field \"" << fieldName << "\" not found" << endl; + break; + case 1: + m_o << "field removed" << endl; + m_modified = true; + break; + default: + m_o << valuesFound << " fields removed" << endl; + m_modified = true; + } + } else { + m_o << "can not remove field; current entry is no account entry" << endl; + } + } else { + m_o << "can not remove field; no file open" << endl; + } +} + +void InteractiveCli::printHelp() +{ + m_o << "Available commands: \n" + "quit,q quits the application\n" + "q! forces the application to quit\n" + "wq saves the current file and quits the application\n" + "clear,c clears the console\n" + "\n" + "create,cr creates a new file at the specified path\n" + "openreadonly opens the specified file (read-only)\n" + "open,o opens the specified file\n" + "close,cl closes the currently opened file\n" + "\n" + "save,w saves the currently opened file\n" + "chpassphrase changes the passphrase\n" + "rmpassphrase removes the passphrase\n" + "\n" + "pwd prints the path of the current entry\n" + "cd changes the current entry\n" + "ls lists the entries/fields of the current entry\n" + "tree,t shows all child entries of the current entry\n" + "mknode,mkn creates a node entry with the specified label in the current entry\n" + "mkaccount,mka creates an account entry with the specified label in the current entry\n" + "rmentry,rme removes the entry specified by its path\n" + "rnentry,rne renames the entry specified by its path\n" + "mventry,me moves the entry specified by its path\n" + "readfield,rf reads the specified field of the current account\n" + "setfield,sf sets the specified field of the current account\n" + "setfieldpw,sp sets the specified password field of the current account\n" + "rmfield,rf removes the specified field of the current account\n" << endl; +} + +void InteractiveCli::quit() +{ + if(m_file.isOpen() && m_modified) { + m_o << "file modified; use q! or wq" << endl; + } else { + m_quit = true; + } +} + +string InteractiveCli::askForPassphrase(bool confirm) +{ + if(confirm) { + m_o << "enter new passphrase: "; + } else { + m_o << "enter passphrase: "; + } + m_o.flush(); + string input1; + { + InputMuter m; + getline(m_i, input1); + } + m_o << endl; + if(input1.empty()) { + m_o << "you did not enter a passphrase" << endl; + } else { + if(confirm) { + m_o << "confirm new passphrase: "; + m_o.flush(); + string input2; + { + InputMuter m; + getline(m_i, input2); + } + m_o << endl; + if(input1 != input2) { + m_o << "phrases do not match" << endl; + throw runtime_error("confirmation failed"); + } + } + } + return input1; +} + +} + diff --git a/cli/cli.h b/cli/cli.h new file mode 100644 index 0000000..c2c546c --- /dev/null +++ b/cli/cli.h @@ -0,0 +1,90 @@ +#ifndef CLI_CLI_H +#define CLI_CLI_H + +#include + +#include + +#if defined(PLATFORM_UNIX) +#include +#elif defined(PLATFORM_WINDOWS) +#include +#endif + +#include +#include +#include +#include + +namespace ApplicationUtilities { + +typedef std::vector StringVector; + +} + +namespace Io { +class Entry; +enum class EntryType : int; +} + +namespace Cli { + +class InputMuter +{ +public: + InputMuter(); + ~InputMuter(); + +private: +#if defined(PLATFORM_UNIX) + termios m_attr; +#elif defined(PLATFORM_WINDOWS) + HANDLE m_cinHandle; + DWORD m_mode; +#endif +}; + +void clearConsole(); + +class InteractiveCli +{ +public: + InteractiveCli(); + void run(const std::string &file = std::string()); + void openFile(const std::string &file, bool readOnly); + void closeFile(); + void saveFile(); + void createFile(const std::string &file); + void changePassphrase(); + void removePassphrase(); + void pwd(); + void cd(const std::string &path); + void ls(); + void tree(); + void makeEntry(Io::EntryType entryType, const std::string &label); + void removeEntry(const std::string &path); + void renameEntry(const std::string &path); + void moveEntry(const std::string &path); + void readField(const std::string &fieldName); + void setField(bool useMuter, const std::string &fieldName); + void removeField(const std::string &fieldName); + void printHelp(); + void quit(); + +private: + void processCommand(const std::string &cmd); + Io::Entry *resolvePath(const std::string &path); + static bool checkCommand(const std::string &str, const char *phrase, std::string ¶m, bool ¶mMissing); + std::string askForPassphrase(bool confirm = false); + + std::ostream &m_o; + std::istream &m_i; + Io::PasswordFile m_file; + Io::Entry *m_currentEntry; + bool m_modified; + bool m_quit; +}; + +} + +#endif // CLI_CLI_H diff --git a/deprecated/draganddroptablewidget.cpp b/deprecated/draganddroptablewidget.cpp new file mode 100644 index 0000000..a00c27d --- /dev/null +++ b/deprecated/draganddroptablewidget.cpp @@ -0,0 +1,81 @@ +#include "draganddroptablewidget.h" + +#include +#include +#include + +namespace QtGui { + +DragAndDropTableWidget::DragAndDropTableWidget(QWidget *parent) : + QTableWidget(parent), + m_insertEmptyRowAtEnd(false) +{ + connect(this, &DragAndDropTableWidget::cellChanged, + this, &DragAndDropTableWidget::processCellChanged); +} + +void DragAndDropTableWidget::insertEmptyRowAtEndAutomatically(bool value) +{ + m_insertEmptyRowAtEnd = value; + if(value) { + processCellChanged(0,0); + } +} + +bool DragAndDropTableWidget::hasEmptyRowAtEnd() const +{ + bool hasEmptyRow = false; + int rowCount = this->rowCount(); + if(rowCount > 0) { + if(QTableWidgetItem *item = this->item(rowCount - 1, 0)) { + hasEmptyRow = item->text().isEmpty(); + } else { + hasEmptyRow = true; + } + } + return hasEmptyRow; +} + +bool DragAndDropTableWidget::dropMimeData(int row, int column, const QMimeData *data, Qt::DropAction action) +{ + if(data->hasText()) { + if(QTableWidgetItem *item = this->item(row, column)) + item->setText(data->text()); + return false; + } else { + return QTableWidget::dropMimeData(row, column, data, action); + } +} + +QStringList DragAndDropTableWidget::mimeTypes() const +{ + return QTableWidget::mimeTypes() << QStringLiteral("text/plain"); +} + +QMimeData *DragAndDropTableWidget::mimeData(const QList items) const +{ + QString text; + if(items.count() > 0) { + QTableWidgetItem *lastItem = items.last(); + foreach(QTableWidgetItem *item, items) { + if(!item->text().isEmpty()) { + text.append(item->text()); + if(item != lastItem) { + text.append(QStringLiteral("\n")); + } + } + } + } + QMimeData *data = QTableWidget::mimeData(items); + data->setText(text); + return data; +} + +void DragAndDropTableWidget::processCellChanged(int, int) +{ + if(m_insertEmptyRowAtEnd && !hasEmptyRowAtEnd()) { + insertRow(rowCount()); + } +} + +} diff --git a/deprecated/draganddroptablewidget.h b/deprecated/draganddroptablewidget.h new file mode 100644 index 0000000..bdea393 --- /dev/null +++ b/deprecated/draganddroptablewidget.h @@ -0,0 +1,33 @@ +#ifndef DRAGANDDROPTABLEWIDGET_H +#define DRAGANDDROPTABLEWIDGET_H + +#include + +namespace QtGui { + +class DragAndDropTableWidget : public QTableWidget +{ + Q_OBJECT +public: + explicit DragAndDropTableWidget(QWidget *parent = nullptr); + + void insertEmptyRowAtEndAutomatically(bool value); + bool hasEmptyRowAtEnd() const; + +protected: + virtual bool dropMimeData(int row, int column, const QMimeData * data, Qt::DropAction action); + virtual QStringList mimeTypes() const; + virtual QMimeData *mimeData(const QList items) const; + +private slots: + void processCellChanged(int, int); + +private: + bool m_insertEmptyRowAtEnd; + + +}; + +} + +#endif // DRAGANDDROPTABLEWIDGET_H diff --git a/general.pri b/general.pri new file mode 100644 index 0000000..9156999 --- /dev/null +++ b/general.pri @@ -0,0 +1,71 @@ +# template +TEMPLATE = lib +#dirs +UI_DIR = ./gui +MOC_DIR = ./moc +OBJECTS_DIR = ./obj +RCC_DIR = ./res +# compiler flags +QMAKE_CXXFLAGS += -std=c++11 +QMAKE_LFLAGS += -std=c++11 +unix { + QMAKE_LFLAGS += "-Wl,--rpath=./" +} +# prefix +targetprefix = . +# target +CONFIG(debug, debug|release) { + TARGET = $$targetprefix/$${projectname}d +} else { + TARGET = $$targetprefix/$$projectname +} +# variables to check target architecture +win32-g++:QMAKE_TARGET.arch = $$QMAKE_HOST.arch +win32-g++-32:QMAKE_TARGET.arch = x86 +win32-g++-64:QMAKE_TARGET.arch = x86_64 +linux-g++:QMAKE_TARGET.arch = $$QMAKE_HOST.arch +linux-g++-32:QMAKE_TARGET.arch = x86 +linux-g++-64:QMAKE_TARGET.arch = x86_64 +# configuration +mobile { + DEFINES += CONFIG_MOBILE +} else:desktop { + DEFINES += CONFIG_DESKTOP +} else:android { + CONFIG += mobile + DEFINES += CONFIG_MOBILE +} else { + CONFIG += desktop + DEFINES += CONFIG_DESKTOP +} +no-gui { + QT -= gui + DEFINES += GUI_NONE + guiqtquick || guiqtwidgets { + error("Can not use no-gui with guiqtquick or guiqtwidgets.") + } else { + message("Configured for no GUI support.") + } +} else { + QT += gui + mobile { + CONFIG += guiqtquick + } + desktop { + CONFIG += guiqtwidgets + } +} +guiqtquick { + message("Configured for Qt Quick GUI support.") + greaterThan(QT_MAJOR_VERSION, 4): QT += quick + CONFIG(debug, debug|release) { + CONFIG += qml_debug + } + DEFINES += GUI_QTQUICK +} +guiqtwidgets { + message("Configured for Qt widgets GUI support.") + greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + DEFINES += GUI_QTWIDGETS + DEFINES += MODEL_UNDO_SUPPORT +} diff --git a/gui/initiate.h b/gui/initiate.h new file mode 100644 index 0000000..4a18815 --- /dev/null +++ b/gui/initiate.h @@ -0,0 +1,16 @@ +#ifndef INITIATE_H +#define INITIATE_H + +#include + +QT_BEGIN_NAMESPACE +class QString; +QT_END_NAMESPACE + +namespace QtGui { + +int runWidgetsGui(int argc, char *argv[], const QString &file); + +} + +#endif // INITIATE_H diff --git a/gui/initiatequi.cpp b/gui/initiatequi.cpp new file mode 100644 index 0000000..e55287e --- /dev/null +++ b/gui/initiatequi.cpp @@ -0,0 +1,42 @@ +#include "initiate.h" + +# include "gui/mainwindow.h" + +#include + +#include +#include +#include + + +namespace QtGui { + +int runWidgetsGui(int argc, char *argv[], const QString &file) +{ + // init application + QApplication a(argc, argv); + QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); + QGuiApplication::setOrganizationName(QStringLiteral("Martchus")); + QGuiApplication::setOrganizationDomain(QStringLiteral("http://martchus.netai.net/")); + QGuiApplication::setApplicationName(QStringLiteral("Password Manager")); + QGuiApplication::setApplicationVersion(QStringLiteral("2.0.5")); + // load translation files + TranslationFiles::loadQtTranslationFile(); + TranslationFiles::loadApplicationTranslationFile(QStringLiteral("passwordmanager")); + // load the other resources + QtUtilitiesResources::init(); + Theme::setup(); + // init widgets GUI + QtGui::MainWindow w; + w.show(); + if(!file.isEmpty()) { + w.openFile(file); + } + // start event loop + int res = a.exec(); + // cleanup resources + QtUtilitiesResources::cleanup(); + return res; +} + +} diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp new file mode 100644 index 0000000..e7121ea --- /dev/null +++ b/gui/mainwindow.cpp @@ -0,0 +1,1171 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "model/fieldmodel.h" +#include "model/entrymodel.h" +#include "model/entryfiltermodel.h" + +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace IoUtilities; +using namespace Io; + +namespace QtGui { + +/*! + * \namespace QtGui + * \brief Contains all GUI related classes and helper functions. + */ + +/*! + * \namespace QtGui::Ui + * \brief Contains all classes generated by the Qt User Interface Compiler (uic). + */ + +/*! + * \class MainWindow + * \brief The MainWindow class provides the main window of the widgets-based GUI of the application. + */ + +/*! + * \brief Constructs a new main window. + */ +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + m_ui(new Ui::MainWindow), + m_clearClipboardTimer(0) +{ + // setup ui + m_ui->setupUi(this); +#ifdef Q_OS_WIN32 + setStyleSheet(QStringLiteral("* { font: 9pt \"Segoe UI\", \"Sans\"; } QMessageBox QLabel, QInputDialog QLabel { font-size: 12pt; color: #003399; } #statusBar { border-top: 1px solid #919191; padding-top: 1px; } #splitter QWidget { background-color: #FFF; } #assumePushButton { font-weight: bold; } #splitter #treeButtonsWidget, #splitter #listButtonsWidget { background-color: #F0F0F0; border-top: 1px solid #DFDFDF; } #leftWidget { border-right: 1px solid #DFDFDF; } #splitter QWidget *, #splitter QWidget * { background-color: none; }")); +#endif + // set default values + m_somethingChanged = false; + m_dontUpdateSelection = false; + updateUiStatus(); + // load settings + QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); + settings.beginGroup(QStringLiteral("mainwindow")); + QStringList recentEntries = settings.value(QStringLiteral("recententries"), QStringList()).toStringList(); + QAction *action = nullptr; + m_ui->actionSepRecent->setSeparator(true); + for(const QString &path : recentEntries) { + if(!path.isEmpty()) { + action = new QAction(path, this); + action->setProperty("file_path", path); + m_ui->menuRecent->insertAction(m_ui->actionSepRecent, action); + connect(action, &QAction::triggered, this, &MainWindow::openRecentFile); + } + } + m_ui->menuRecent->setEnabled(action); + // set position and size + resize(settings.value("size", size()).toSize()); + move(settings.value("pos", QPoint(300, 200)).toPoint()); + // setup undo stack and related actions + m_undoStack = new QUndoStack(this); + m_undoView = nullptr; + m_ui->actionUndo->setShortcuts(QKeySequence::Undo); + m_ui->actionRedo->setShortcuts(QKeySequence::Redo); + // setup models, tree and table view + m_ui->treeView->setModel(m_entryFilterModel = new EntryFilterModel(this)); + m_ui->tableView->setModel(m_fieldModel = new FieldModel(m_undoStack, this)); + m_fieldModel->setHidePasswords(settings.value("hidepasswords", true).toBool()); + m_entryFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_entryFilterModel->setSourceModel(m_entryModel = new EntryModel(m_undoStack, this)); +#ifdef Q_OS_WIN32 + m_ui->treeView->setFrameShape(QFrame::NoFrame); + m_ui->tableView->setFrameShape(QFrame::NoFrame); +#else + m_ui->treeView->setFrameShape(QFrame::StyledPanel); + m_ui->tableView->setFrameShape(QFrame::StyledPanel); +#endif + m_ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + // splitter sizes + m_ui->splitter->setSizes(QList() << 100 << 800); + // connect signals and slots + // file related actions + connect(m_ui->actionSave, &QAction::triggered, this, &MainWindow::saveFile); + connect(m_ui->actionExport, &QAction::triggered, this, &MainWindow::exportFile); + connect(m_ui->actionShowContainingDirectory, &QAction::triggered, this, &MainWindow::showContainingDirectory); + connect(m_ui->actionClose, &QAction::triggered, this, &MainWindow::closeFile); + connect(m_ui->actionCreate, &QAction::triggered, this, static_cast(&MainWindow::createFile)); + connect(m_ui->actionQuit, &QAction::triggered, this, &MainWindow::close); + connect(m_ui->actionChangepassword, &QAction::triggered, this, &MainWindow::changePassword); + // showing dialogs + connect(m_ui->actionPasswordGenerator, &QAction::triggered, this, &MainWindow::showPassowrdGeneratorDialog); + connect(m_ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutDialog); + connect(m_ui->actionOpen, &QAction::triggered, this, &MainWindow::showOpenFileDialog); + connect(m_ui->actionSaveAs, &QAction::triggered, this, &MainWindow::showSaveFileDialog); + // recent menu + connect(m_ui->actionClearRecent, &QAction::triggered, this, &MainWindow::clearRecent); + // add/remove account + connect(m_ui->actionAddAccount, &QAction::triggered, this, &MainWindow::addAccount); + connect(m_ui->actionAddCategory, &QAction::triggered, this, &MainWindow::addCategory); + connect(m_ui->actionRemoveRows, &QAction::triggered, this, &MainWindow::removeEntry); + // insert/remove fields + connect(m_ui->actionInsertRow, &QAction::triggered, this, &MainWindow::insertRow); + connect(m_ui->actionRemoveAccount, &QAction::triggered, this, &MainWindow::removeRows); + // undo/redo + connect(m_ui->actionUndo, &QAction::triggered, m_undoStack, &QUndoStack::undo); + connect(m_ui->actionRedo, &QAction::triggered, m_undoStack, &QUndoStack::redo); + connect(m_undoStack, &QUndoStack::canUndoChanged, m_ui->actionUndo, &QAction::setEnabled); + connect(m_undoStack, &QUndoStack::canRedoChanged, m_ui->actionRedo, &QAction::setEnabled); + // view + connect(m_ui->actionHidePasswords, &QAction::triggered, m_fieldModel, &FieldModel::setHidePasswords); + connect(m_ui->actionShowUndoStack, &QAction::triggered, this, &MainWindow::showUndoView); + // models + connect(m_ui->treeView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::accountSelected); + connect(m_entryModel, &QAbstractItemModel::dataChanged, this, &MainWindow::setSomethingChanged); + connect(m_fieldModel, &QAbstractItemModel::dataChanged, this, &MainWindow::setSomethingChanged); + // context menus + connect(m_ui->treeView, &QTableView::customContextMenuRequested, this, &MainWindow::showTreeViewContextMenu); + connect(m_ui->tableView, &QTableView::customContextMenuRequested, this, &MainWindow::showTableViewContextMenu); + // filter + //connect(m_ui->accountFilterLineEdit, &QLineEdit::textChanged, m_entryFilterModel, &QSortFilterProxyModel::setFilterFixedString); + connect(m_ui->accountFilterLineEdit, &QLineEdit::textChanged, this, &MainWindow::applyFilter); + // setup other controls + m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), false).toBool()); + m_ui->accountFilterLineEdit->setText(settings.value(QStringLiteral("accountfilter"), QString()).toString()); + m_ui->actionHidePasswords->setChecked(m_fieldModel->hidePasswords()); + settings.endGroup(); +} + +/*! + * \brief Destroys the main window. + */ +MainWindow::~MainWindow() +{} + +bool MainWindow::eventFilter(QObject *obj, QEvent *event) +{ + if(obj == m_undoView) { + switch(event->type()) { + case QEvent::Hide: + m_ui->actionShowUndoStack->setChecked(false); + break; + default: + ; + } + } + return QMainWindow::eventFilter(obj, event); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + // ask if file is opened + if(m_file.hasRootEntry()) { + if(!closeFile()) { + event->ignore(); + return; + } + } + // close undow view + if(m_undoView) { + m_undoView->close(); + } + // save settings + QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); + settings.beginGroup(QStringLiteral("mainwindow")); + settings.setValue(QStringLiteral("size"), size()); + settings.setValue(QStringLiteral("pos"), pos()); + QStringList existingEntires; + QList entryActions = m_ui->menuRecent->actions(); + existingEntires.reserve(entryActions.size()); + for(const QAction *action : entryActions) { + QVariant path = action->property("file_path"); + if(!path.isNull()) { + existingEntires << path.toString(); + } + } + settings.setValue(QStringLiteral("recententries"), existingEntires); + settings.setValue(QStringLiteral("accountfilter"), m_ui->accountFilterLineEdit->text()); + settings.setValue(QStringLiteral("alwayscreatebackup"), m_ui->actionAlwaysCreateBackup->isChecked()); + settings.setValue(QStringLiteral("hidepasswords"), m_ui->actionHidePasswords->isChecked()); + settings.endGroup(); +} + +void MainWindow::timerEvent(QTimerEvent *event) +{ + if(event->timerId() == m_clearClipboardTimer) { + clearClipboard(); + m_clearClipboardTimer = 0; + } +} + +/*! + * \brief Shows the about dialog. + */ +void MainWindow::showAboutDialog() +{ + using namespace Dialogs; + AboutDialog* aboutDlg = new AboutDialog(this, tr("A simple password store using AES-256-CBC encryption via OpenSSL."), QImage(":/icons/hicolor/128x128/apps/passwordmanager.png")); + aboutDlg->show(); +} + +/*! + * \brief Shows the password generator dialog. + */ +void MainWindow::showPassowrdGeneratorDialog() +{ + PasswordGeneratorDialog* pwgDialog = new PasswordGeneratorDialog(this); + pwgDialog->show(); +} + +/*! + * \brief Shows the open file dialog and opens the selected file. + */ +void MainWindow::showOpenFileDialog() +{ + if(m_file.hasRootEntry() && !closeFile()) { + return; + } + QString fileName = QFileDialog::getOpenFileName(this, tr("Select a password list")); + if(!fileName.isEmpty()) { + openFile(fileName); + } +} + +/*! + * \brief Opens a file from the "recently opened" list. + * + * This private slot is directly called when the corresponding QAction is triggered. + */ +void MainWindow::openRecentFile() +{ + if(QAction* action = qobject_cast(sender())) { + QString path = action->property("file_path").toString(); + if(!path.isEmpty()) { + if(QFile::exists(path)) { + openFile(path); + } else { + QMessageBox msg(this); + msg.setWindowTitle(QApplication::applicationName()); + msg.setText(tr("The selected file can't be found anymore. Do you want to delete the obsolete entry from the list?")); + msg.setIcon(QMessageBox::Warning); + QPushButton *keepEntryButton = msg.addButton(tr("keep entry"), QMessageBox::NoRole); + QPushButton *deleteEntryButton = msg.addButton(tr("delete entry"), QMessageBox::YesRole); + msg.setEscapeButton(keepEntryButton); + msg.exec(); + if(msg.clickedButton() == deleteEntryButton) { + delete action; + } + } + } + } +} + +/*! + * \brief Shows the save file dialog and saves the file at the selected location. + */ +void MainWindow::showSaveFileDialog() +{ + if(showNoFileOpened()) { + return; + } + if(askForCreatingFile()) { + saveFile(); + } +} + +/*! + * \brief Shows the undo view. + */ +void MainWindow::showUndoView() +{ + if(m_ui->actionShowUndoStack->isChecked()) { + if(!m_undoView) { + m_undoView = new QUndoView(m_undoStack); + m_undoView->setWindowTitle(tr("Undo stack")); + m_undoView->setWindowFlags(Qt::Tool); + m_undoView->setAttribute(Qt::WA_QuitOnClose); + m_undoView->setWindowIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); + m_undoView->installEventFilter(this); + } + m_undoView->show(); + } else if(m_undoView) { + m_undoView->hide(); + } +} + +/*! + * \brief Opens a file with the specified \a path and updates all widgets to show its contents. + * \returns Returns true on success; otherwise false + */ +bool MainWindow::openFile(const QString &path) +{ + using namespace Dialogs; + // close previous file + if(m_file.hasRootEntry() && !closeFile()) { + return false; + } + // set path and open file + m_file.setPath(path.toStdString()); + try { + m_file.open(); + } catch (ios_base::failure &ex) { + QString errmsg = tr("An IO error occured when opening the specified file \"%1\".\n\n(%2)").arg(path, QString::fromLocal8Bit(ex.what())); + m_ui->statusBar->showMessage(errmsg, 5000); + QMessageBox::critical(this, QApplication::applicationName(), errmsg); + return false; + } + // warn before loading a very big file + if(m_file.size() > 10485760) { + if(QMessageBox::warning(this, QApplication::applicationName(), tr("The file you want to load seems to be very big. Do you really want to open it?"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { + m_file.clear(); + return false; + } + } + // ask for a password if required + if(m_file.isEncryptionUsed()) { + EnterPasswordDialog pwDlg(this); + pwDlg.setWindowTitle(QApplication::applicationName()); + pwDlg.setInstruction(tr("Enter the password to open the file")); + pwDlg.setPasswordRequired(true); + switch(pwDlg.exec()) { + case QDialog::Accepted: + if(pwDlg.password().isEmpty()) { + m_ui->statusBar->showMessage(tr("A password is needed to open the file."), 5000); + QMessageBox::warning(this, QApplication::applicationName(), tr("A password is needed to open the file.")); + m_file.clear(); + return false; + } else { + break; + } + case QDialog::Rejected: + m_file.clear(); + return false; + default: + ; + } + m_file.setPassword(pwDlg.password().toStdString()); + } + // load the contents of the file + QString msg; + try { + m_file.load(); + } catch(CryptoException &ex) { + msg = tr("The file couldn't be decrypted.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(ex.what())); + } catch(ios_base::failure &ex) { + msg = QString::fromLocal8Bit(ex.what()); + } catch(runtime_error &ex) { + msg = tr("Unable to parse the file. %1").arg(QString::fromLocal8Bit(ex.what())); + } + // show a message in the error case + if(!msg.isEmpty()) { + m_file.clear(); + m_ui->statusBar->showMessage(msg, 5000); + if(QMessageBox::critical(this, QApplication::applicationName(), msg, + QMessageBox::Cancel, QMessageBox::Retry) == QMessageBox::Retry) { + return openFile(path); // retry + } else { + return false; + } + } else { + // show contents + return showFile(); + } +} + +/*! + * \brief Creates a new file. + * \returns Returns true on success; otherwise false + */ +bool MainWindow::createFile() +{ + // close previous file + if(m_file.hasRootEntry() && !closeFile()) { + return false; + } + m_file.generateRootEntry(); + return showFile(); +} + +/*! + * \brief Creates a new file with the specified \a path. + * \returns Returns true on success; otherwise false + */ +void MainWindow::createFile(const QString &path) +{ + createFile(path, QString()); +} + +/*! + * \brief Creates a new file with the specified \a path and \a password. + * \returns Returns true on success; otherwise false + */ +void MainWindow::createFile(const QString &path, const QString &password) +{ + // close previous file + if(m_file.hasRootEntry() && !closeFile()) { + return; + } + // set path and password + m_file.setPath(path.toStdString()); + m_file.setPassword(password.toStdString()); + // create the file and show it + try { + m_file.create(); + } catch (ios_base::failure) { + QMessageBox::critical(this, QApplication::applicationName(), tr("The file %1 couldn't be created.").arg(path)); + return; + } + m_file.generateRootEntry(); + showFile(); +} + +/*! + * \brief Shows the previously opened file. Called within openFile() and createFile(). + * \returns Returns true on success; otherwise false + */ +bool MainWindow::showFile() +{ + m_fieldModel->reset(); + m_entryModel->setRootEntry(m_file.rootEntry()); + applyDefaultExpanding(QModelIndex()); + if(m_file.path().empty()) { + m_ui->statusBar->showMessage(tr("A new password list has been created."), 5000); + } else { + QString title = QString::fromStdString(m_file.path()); + addRecentEntry(title); + m_ui->statusBar->showMessage(tr("The password list \"%1\" has been load.").arg(title), 5000); + setWindowTitle(QStringLiteral("%1 - %2").arg(title, QApplication::applicationName())); + } + updateUiStatus(); + applyFilter(m_ui->accountFilterLineEdit->text()); + m_somethingChanged = false; + return true; +} + +/*! + * \brief Adds a recent entry for the specified \a path. Called within showFile(). + */ +void MainWindow::addRecentEntry(const QString &path) +{ + // check if the path already exists + QList existingEntries = m_ui->menuRecent->actions(); + QAction *entry = nullptr; + for(QAction *existingEntry : existingEntries) { + if(existingEntry->property("file_path").toString() == path) { + entry = existingEntry; + break; + } + } + if(!entry) { + // remove old entries to have never more then 8 entries + for(int i = existingEntries.size(); i > 9; --i) { + delete existingEntries.last(); + } + existingEntries = m_ui->menuRecent->actions(); + // create new action + entry = new QAction(path, this); + connect(entry, &QAction::triggered, this, &MainWindow::openRecentFile); + } else { + // remove existing action (will be inserted again as first action) + m_ui->menuRecent->removeAction(entry); + } + // ensure menu is enabled + m_ui->menuRecent->setEnabled(true); + // add action as first action in the recent menu + m_ui->menuRecent->insertAction(existingEntries.isEmpty() ? nullptr : existingEntries.first(), entry); +} + +/*! + * \brief Updates the status of the UI elements. + */ +void MainWindow::updateUiStatus() +{ + bool fileOpened = m_file.hasRootEntry(); + m_ui->actionCreate->setEnabled(true); + m_ui->actionOpen->setEnabled(true); + m_ui->actionSave->setEnabled(fileOpened); + m_ui->actionSaveAs->setEnabled(fileOpened); + m_ui->actionExport->setEnabled(fileOpened); + m_ui->actionShowContainingDirectory->setEnabled(fileOpened); + m_ui->actionClose->setEnabled(fileOpened); + m_ui->actionChangepassword->setEnabled(fileOpened); + m_ui->menuEdit->setEnabled(fileOpened); + m_ui->accountFilterLineEdit->setEnabled(true); +} + +void MainWindow::applyDefaultExpanding(const QModelIndex &parent) +{ + for(int row = 0, rows = m_entryFilterModel->rowCount(parent); row < rows; ++row) { + QModelIndex index = m_entryFilterModel->index(row, 0, parent); + if(!index.isValid()) { + return; + } + applyDefaultExpanding(index); + m_ui->treeView->setExpanded(index, m_entryFilterModel->data(index, DefaultExpandedRole).toBool()); + } +} + +/*! + * \brief Returns a string with the values of all selected fields. + * \remarks Columns are sparated with \t, rows with \n. + */ +QString MainWindow::selectedFieldsString() const +{ + QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); + QString text; + if(!selectedIndexes.isEmpty()) { + if(selectedIndexes.size() > 1) { + int maxRow = m_fieldModel->rowCount() - 1; + int firstRow = maxRow, lastRow = 0; + int maxCol = m_fieldModel->columnCount() - 1; + int firstCol = maxCol, lastCol = 0; + for(const QModelIndex &index : selectedIndexes) { + if(index.row() < firstRow) { + firstRow = index.row(); + } + if(index.row() > lastRow) { + lastRow = index.row(); + } + if(index.column() < firstCol) { + firstCol = index.column(); + } + if(index.column() > lastCol) { + lastCol = index.column(); + } + } + for(int row = firstRow; row <= lastRow; ++row) { + for(int col = firstCol; col <= lastCol; ++col) { + QModelIndex index = m_fieldModel->index(row, col); + if(selectedIndexes.contains(index)) { + text.append(index.data(Qt::EditRole).toString()); + } + text.append('\t'); + } + text.append('\n'); + } + } else { + text = selectedIndexes.front().data(Qt::EditRole).toString(); + } + } + return text; +} + +/*! + * \brief Inserts fields from the specified \a fieldsString. + */ +void MainWindow::insertFields(const QString &fieldsString) +{ + QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); + if(selectedIndexes.size() == 1) { + int rows = m_fieldModel->rowCount(), cols = m_fieldModel->columnCount(); + int row = selectedIndexes.front().row(); + int initCol = selectedIndexes.front().column(); + assert(row < rows); + QStringList rowValues = fieldsString.split('\n'); + if(rowValues.back().isEmpty()) { + rowValues.pop_back(); + } + m_fieldModel->insertRows(row, rowValues.size(), QModelIndex()); + for(const QString &rowValue : rowValues) { + int col = initCol; + for(const QString &cellValue : rowValue.split('\t')) { + if(col < cols) { + m_fieldModel->setData(m_fieldModel->index(row, col), cellValue, Qt::EditRole); + ++col; + } else { + break; + } + } + ++row; + } + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("Exactly one fields needs to be selected (top-left corner for insertion).")); + } +} + +/*! + * \brief Asks the user to create a new file. + */ +bool MainWindow::askForCreatingFile() +{ + if(showNoFileOpened()) { + return false; + } + + QString fileName = + QFileDialog::getSaveFileName( + this, + tr("Select where you want to save the password list"), + QString(), + tr("All files (*.*)")); + if(fileName.isEmpty()) { + m_ui->statusBar->showMessage(tr("The file was not be saved."), 7000); + return false; + } else { + m_file.setPath(fileName.toStdString()); + try { + m_file.create(); + } catch (ios_base::failure &ex) { + QMessageBox::critical(this, QApplication::applicationName(), QString::fromLocal8Bit(ex.what())); + return false; + } + } + return true; +} + +/*! + * \brief Shows an warning if no file is opened. + * \retruns Returns whether the warning has been shown. + */ +bool MainWindow::showNoFileOpened() +{ + if(!m_file.hasRootEntry()) { + QMessageBox::warning(this, QApplication::applicationName(), tr("There is no password list opened.")); + return true; + } + return false; +} + +/*! + * \brief Shows an warning if no account is selected. + * \retruns Returns whether the warning has been shown. + */ +bool MainWindow::showNoAccount() +{ + if(!m_fieldModel->fields()) { + QMessageBox::warning(this, QApplication::applicationName(), tr("There's no account selected.")); + return true; + } + return false; +} + +/*! + * \brief Closes the currently opened file. Asks the user to save changes if the file has been modified. + * \returns Returns whether the file has been closed. + */ +bool MainWindow::closeFile() +{ + if(showNoFileOpened()) { + return false; + } + if(m_somethingChanged) { + QMessageBox msg(this); + msg.setText(tr("The password file has been modified.")); + msg.setInformativeText(tr("Do you want to save the changes before closing?")); + msg.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + msg.setDefaultButton(QMessageBox::Save); + msg.setIcon(QMessageBox::Warning); + switch (msg.exec()) { + case QMessageBox::Save: + if(saveFile()) { + break; + } else { + return false; + } + case QMessageBox::Cancel: + return false; + default: + ; + } + } + m_fieldModel->reset(); + m_entryModel->reset(); + m_file.clear(); + m_ui->statusBar->showMessage(tr("The password list has been closed.")); + setWindowTitle(QApplication::applicationName()); + updateUiStatus(); + m_somethingChanged = false; + return true; +} + +/*! + * \brief Saves the currently opened file. + * \returns Returns whether the file could be saved. + */ +bool MainWindow::saveFile() +{ + using namespace Dialogs; + if(showNoFileOpened()) { + return false; + } + // create backup + if(!m_file.path().empty() && QFile::exists(QString::fromStdString(m_file.path()))) { + if(m_ui->actionAlwaysCreateBackup->isChecked()) { + try { + m_file.doBackup(); + } catch(ios_base::failure &ex) { + QString message(tr("The backup file couldn't be created. %1").arg(QString::fromLocal8Bit(ex.what()))); + QMessageBox::critical(this, QApplication::applicationName(), message); + m_ui->statusBar->showMessage(message, 7000); + return false; + } + } + } else { + if(!askForCreatingFile()) { + return false; + } + } + // ask for a password if none is set + if(m_file.password()[0] == 0) { + EnterPasswordDialog pwDlg(this); + pwDlg.setWindowTitle(QApplication::applicationName()); + pwDlg.setInstruction(tr("Enter a password to save the file")); + pwDlg.setVerificationRequired(true); + + switch(pwDlg.exec()) { + case QDialog::Accepted: + m_file.setPassword(pwDlg.password().toStdString()); + break; + default: + m_ui->statusBar->showMessage(tr("The file hasn't been saved."), 7000); + return false; + } + } + // save the file + QString msg; + try { + m_file.save(m_file.password()[0] != 0); + } catch (CryptoException &ex) { + msg = tr("The password list couldn't be saved due to encryption failure.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(ex.what())); + } catch(ios_base::failure &ex) { + msg = QString::fromLocal8Bit(ex.what()); + } + // show status + if(!msg.isEmpty()) { + m_ui->statusBar->showMessage(msg, 5000); + QMessageBox::critical(this, QApplication::applicationName(), msg); + return false; + } else { + m_somethingChanged = false; + addRecentEntry(QString::fromStdString(m_file.path())); + m_ui->statusBar->showMessage(tr("The password list has been saved."), 5000); + return true; + } +} + +/*! + * \brief Exports the files contents to a plain text file. + */ +void MainWindow::exportFile() +{ + if(showNoFileOpened()) { + return; + } + QString targetPath = QFileDialog::getSaveFileName(this, QApplication::applicationName()); + if(!targetPath.isNull()) { + QString errmsg; + try { + m_file.exportToTextfile(targetPath.toStdString()); + } catch (ios_base::failure &ex) { + errmsg = tr("The password list couldn't be exported. %1").arg(QString::fromLocal8Bit(ex.what())); + } + if(errmsg.isEmpty()) { + m_ui->statusBar->showMessage(tr("The password list has been exported."), 5000); + } else { + m_ui->statusBar->showMessage(errmsg, 5000); + QMessageBox::critical(this, QApplication::applicationName(), errmsg); + } + } +} + +/*! + * \brief Shows the containing directory for the currently opened file. + */ +void MainWindow::showContainingDirectory() +{ + if(showNoFileOpened()) { + return; + } else if(m_file.path().empty()) { + QMessageBox::warning(this, QApplication::applicationName(), tr("The currently opened file hasn't been saved yet.")); + } else { + QFileInfo file(QString::fromStdString(m_file.path())); + if(file.dir().exists()) { + QDesktopServices::openUrl(file.dir().absolutePath()); + } + } +} + +/*! + * \brief Adds a new account entry to the selected category. + */ +void MainWindow::addAccount() +{ + addEntry(EntryType::Account); +} + +/*! + * \brief Adds a new category/node entry to the selected category. + */ +void MainWindow::addCategory() +{ + addEntry(EntryType::Node); +} + +/*! + * \brief Adds a new entry to the selected category. + * \param type Specifies the type of the entry to be created. + * \param title Specifies the title of the user prompt which will be shown to ask for the entry label. + */ +void MainWindow::addEntry(EntryType type) +{ + if(showNoFileOpened()) { + return; + } + QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); + if(selectedIndexes.size() == 1) { + QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0)); + if(m_entryModel->isNode(selected)) { + bool result; + QString text = QInputDialog::getText(this, type == EntryType::Account ? tr("Add account") : tr("Add category"), tr("Enter the entry name"), QLineEdit::Normal, tr("new entry"), &result); + if (result) { + if(!text.isEmpty()) { + int row = m_entryModel->rowCount(selected); + m_entryModel->setInsertType(type); + if(m_entryModel->insertRow(row, selected)) { + m_entryModel->setData(m_entryModel->index(row, 0, selected), text, Qt::DisplayRole); + setSomethingChanged(); + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to create new entry.")); + } + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter text.")); + } + } + return; + } + } + QMessageBox::warning(this, QApplication::applicationName(), tr("No node element selected.")); +} + +/*! + * \brief Removes the selected entry. + */ +void MainWindow::removeEntry() +{ + if(showNoFileOpened()) { + return; + } + QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); + if(selectedIndexes.size() == 1) { + QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0)); + if(!m_entryModel->removeRow(selected.row(), selected.parent())) { + QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to remove the entry.")); + } + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("No entry selected.")); + } +} + +/*! + * \brief Applies the entered filter. + * \remarks Called when the textChanged signal of m_ui->accountFilterLineEdit is emittet. + */ +void MainWindow::applyFilter(const QString &filterText) +{ + if(filterText.isEmpty()) { + applyDefaultExpanding(QModelIndex()); + } else { + m_ui->treeView->expandAll(); + } + m_entryFilterModel->setFilterRegExp(filterText); +} + +/*! + * \brief Called when the user \a selected an entry. + */ +void MainWindow::accountSelected(const QModelIndex &selected, const QModelIndex &) +{ + if(Entry *entry = m_entryModel->entry(m_entryFilterModel->mapToSource(selected))) { + if(entry->type() == EntryType::Account) { + m_fieldModel->setAccountEntry(static_cast(entry)); + return; + } + } + m_fieldModel->setAccountEntry(nullptr); +} + +/*! + * \brief Inserts an empty row before the selected one. + */ +void MainWindow::insertRow() +{ + if(showNoFileOpened() || showNoAccount()) { + return; + } + QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); + if(selectedIndexes.size()) { + int row = m_fieldModel->rowCount(); + foreach(const QModelIndex &index, selectedIndexes) { + if(index.row() < row) { + row = index.row(); + } + } + if(row < m_fieldModel->rowCount() - 1) { + m_fieldModel->insertRow(row); + } + } else { + QMessageBox::warning(this, windowTitle(), tr("A field has to be selected since new fields are always inserted before the currently selected field.")); + } +} + +/*! + * \brief Removes the selected rows. + */ +void MainWindow::removeRows() +{ + if(showNoFileOpened() || showNoAccount()) { + return; + } + QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); + QList rows; + foreach(const QModelIndex &index, selectedIndexes) { + rows << index.row(); + } + if(rows.size()) { + for(int i = m_fieldModel->rowCount() - 1; i >= 0; --i) { + if(rows.contains(i)) { + m_fieldModel->removeRow(i); + } + } + } else { + QMessageBox::warning(this, windowTitle(), tr("No fields have been removed since there are currently no fields selected.")); + } +} + +/*! + * \brief Marks the selected field as password field. + */ +void MainWindow::markAsPasswordField() +{ + setFieldType(FieldType::Password); +} + +/*! + * \brief Marks the selected field as normal field. + */ +void MainWindow::markAsNormalField() +{ + setFieldType(FieldType::Normal); +} + +/*! + * \brief Sets the type of the selected field to the specified \a fieldType. + */ +void MainWindow::setFieldType(FieldType fieldType) +{ + if(showNoFileOpened() || showNoAccount()) { + return; + } + QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); + if(!selectedIndexes.isEmpty()) { + QVariant typeVariant(static_cast(fieldType)); + foreach(const QModelIndex &index, selectedIndexes) { + m_fieldModel->setData(index, typeVariant, FieldTypeRole); + } + } else { + QMessageBox::warning(this, windowTitle(), tr("No fields have been changed since there are currently no fields selected.")); + } +} + +/*! + * \brief Asks the user to change the password which will be used when calling saveFile() next time. + */ +void MainWindow::changePassword() +{ + using namespace Dialogs; + if(showNoFileOpened()) return; + + EnterPasswordDialog pwDlg(this); + pwDlg.setWindowTitle(QApplication::applicationName()); + pwDlg.setVerificationRequired(true); + + switch(pwDlg.exec()) { + case QDialog::Accepted: + if(pwDlg.password().isEmpty()) { + m_file.clearPassword(); + setSomethingChanged(); + QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter a password. No encryption will be used when saving the file next time.")); + } else { + m_file.setPassword(pwDlg.password().toStdString()); + setSomethingChanged(); + QMessageBox::warning(this, QApplication::applicationName(), tr("The new password will be used next time you save the file.")); + } + break; + default: + QMessageBox::warning(this, QApplication::applicationName(), tr("You aborted. The old password will still be used when saving the file next time.")); + } +} + +/*! + * \brief Clears all entries in the "recently opened" list. + */ +void MainWindow::clearRecent() +{ + QList entries = m_ui->menuRecent->actions(); + for(auto i = entries.begin(), end = entries.end() - 2; i != end; ++i) { + if(*i != m_ui->actionClearRecent) { + delete *i; + } + } + m_ui->menuRecent->setEnabled(false); +} + +/*! + * \brief Shows the tree view context menu. + */ +void MainWindow::showTreeViewContextMenu() +{ + if(!m_file.hasRootEntry()) { + return; + } + QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); + if(selectedIndexes.size() == 1) { + QMenu contextMenu(this); + QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0)); + Entry *entry = m_entryModel->entry(selected); + if(entry->type() == EntryType::Node) { + contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add account"), this, SLOT(addAccount())); + contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add category"), this, SLOT(addCategory())); + } + contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), tr("Remove entry"), this, SLOT(removeEntry())); + if(entry->type() == EntryType::Node) { + NodeEntry *nodeEntry = static_cast(entry); + contextMenu.addSeparator(); + QAction *action = new QAction(&contextMenu); + action->setCheckable(true); + action->setText(tr("Expanded by default")); + action->setChecked(nodeEntry->isExpandedByDefault()); + connect(action, &QAction::triggered, std::bind(&EntryModel::setData, m_entryModel, std::cref(selected), QVariant(!nodeEntry->isExpandedByDefault()), DefaultExpandedRole)); + contextMenu.addAction(action); + } + contextMenu.exec(QCursor::pos()); + } +} + +/*! + * \brief Shows the table view context menu. + */ +void MainWindow::showTableViewContextMenu() +{ + QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); + if(!m_file.hasRootEntry() || !m_fieldModel->fields() || selectedIndexes.isEmpty()) { + return; + } + QMenu contextMenu(this); + FieldType firstType; + bool allOfSameType = true; + bool hasOneFieldType = false; + int row = selectedIndexes.front().row(); + int multipleRows = 1; + foreach(const QModelIndex &index, selectedIndexes) { + if(const Field *field = m_fieldModel->field(index.row())) { + if(hasOneFieldType) { + if(firstType != field->type()) { + allOfSameType = false; + break; + } + } else { + firstType = field->type(); + hasOneFieldType = true; + } + } + if(multipleRows == 1 && index.row() != row) { + ++multipleRows; + } + } + // insertion and removal + contextMenu.addAction(QIcon::fromTheme("list-add"), tr("Insert field(s)", nullptr, multipleRows), this, SLOT(insertRow())); + contextMenu.addAction(QIcon::fromTheme("list-remove"), tr("Remove field(s)", nullptr, multipleRows), this, SLOT(removeRows())); + // show the "Mark as ..." action only when all selected indexes are of the same type + if(hasOneFieldType && allOfSameType) { + switch(firstType) { + case FieldType::Normal: + contextMenu.addAction(tr("Mark as password field"), this, SLOT(markAsPasswordField())); + break; + case FieldType::Password: + contextMenu.addAction(tr("Mark as normal field"), this, SLOT(markAsNormalField())); + break; + } + } + contextMenu.addSeparator(); + contextMenu.addAction(QIcon::fromTheme("edit-copy"), tr("Copy"), this, SLOT(copyFields())); + contextMenu.addAction(QIcon::fromTheme("edit-copy"), tr("Copy for 5 seconds"), this, SLOT(copyFieldsForXMilliSeconds())); + if(QApplication::clipboard()->mimeData()->hasText()) { + contextMenu.addAction(QIcon::fromTheme("edit-paste"), tr("Paste"), this, SLOT(insertFieldsFromClipboard())); + } + contextMenu.exec(QCursor::pos()); +} + +/*! + * \brief Copies the selected cells to the clipboard and clears the clipboard after \a x milli seconds again. + */ +void MainWindow::copyFieldsForXMilliSeconds(int x) +{ + QString text = selectedFieldsString(); + if(!text.isEmpty()) { + if(m_clearClipboardTimer) { + killTimer(m_clearClipboardTimer); + } + QApplication::clipboard()->setText(text); + if(x > 0) { + m_clearClipboardTimer = startTimer(x, Qt::CoarseTimer); + } + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("The selection is empty.")); + } +} + +/*! + * \brief Copies the selected cells to the clipboard. + */ +void MainWindow::copyFields() +{ + copyFieldsForXMilliSeconds(-1); +} + +/*! + * \brief Inserts fields from the clipboard. + */ +void MainWindow::insertFieldsFromClipboard() +{ + insertFields(QApplication::clipboard()->text()); +} + +/*! + * \brief Clears the clipboard. + */ +void MainWindow::clearClipboard() +{ + QApplication::clipboard()->clear(); +} + +/*! + * \brief Sets the variable m_somethingChanged to true. + */ +void MainWindow::setSomethingChanged() +{ + m_somethingChanged = true; +} + +} diff --git a/gui/mainwindow.h b/gui/mainwindow.h new file mode 100644 index 0000000..df6d0ec --- /dev/null +++ b/gui/mainwindow.h @@ -0,0 +1,129 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "passwordgeneratordialog.h" + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QCloseEvent; +class QTreeWidgetItem; +class QUndoStack; +class QUndoView; +QT_END_NAMESPACE + +namespace Io { +DECLARE_ENUM(EntryType, int) +DECLARE_ENUM(FieldType, int) +} + +namespace QtGui { + +class FieldModel; +class EntryModel; +class EntryFilterModel; + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + // file management + bool openFile(const QString &path); + void createFile(const QString &path, const QString &password); + void createFile(const QString &path); + +public slots: + // file management + bool createFile(); + void changePassword(); + bool saveFile(); + void exportFile(); + bool closeFile(); + +protected: + bool eventFilter(QObject *obj, QEvent *event); + void closeEvent(QCloseEvent *event); + void timerEvent(QTimerEvent *event); + +private slots: + // showing dialogs + void showAboutDialog(); + void showPassowrdGeneratorDialog(); + void showOpenFileDialog(); + void showSaveFileDialog(); + void showUndoView(); + // file management + bool showFile(); + // account/categories management + void addAccount(); + void addCategory(); + void addEntry(Io::EntryType type); + void removeEntry(); + void applyFilter(const QString &filterText); + // row management + void accountSelected(const QModelIndex &selected, const QModelIndex &); + void insertRow(); + void removeRows(); + void markAsPasswordField(); + void markAsNormalField(); + void setFieldType(Io::FieldType fieldType); + QString selectedFieldsString() const; + void insertFields(const QString &fieldsString); + void copyFieldsForXMilliSeconds(int x = 5000); + void copyFields(); + void insertFieldsFromClipboard(); + // showing context menus + void showTreeViewContextMenu(); + void showTableViewContextMenu(); + // recent entries menu + void addRecentEntry(const QString &path); + void openRecentFile(); + void clearRecent(); + // other + void showContainingDirectory(); + void clearClipboard(); + void setSomethingChanged(); + +private: + // showing conditional messages/prompts + bool askForCreatingFile(); + bool showNoFileOpened(); + bool showNoAccount(); + // other + void updateUiStatus(); + void applyDefaultExpanding(const QModelIndex &parent); + + std::unique_ptr m_ui; + Io::PasswordFile m_file; + FieldModel *m_fieldModel; + EntryModel *m_entryModel; + EntryFilterModel *m_entryFilterModel; + QUndoStack *m_undoStack; + QUndoView *m_undoView; + bool m_somethingChanged; + bool m_dontUpdateSelection; + int m_clearClipboardTimer; +}; + +} + +#endif // MAINWINDOW_H diff --git a/gui/mainwindow.ui b/gui/mainwindow.ui new file mode 100644 index 0000000..b670e06 --- /dev/null +++ b/gui/mainwindow.ui @@ -0,0 +1,516 @@ + + + QtGui::MainWindow + + + + 0 + 0 + 848 + 444 + + + + Password Manager + + + + :/icons/hicolor/128x128/apps/passwordmanager.png:/icons/hicolor/128x128/apps/passwordmanager.png + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 1 + + + 0 + + + + + + 0 + 0 + + + + + 225 + 0 + + + + filter + + + + + + + Qt::CustomContextMenu + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + true + + + true + + + true + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QFrame::Raised + + + true + + + QAbstractItemView::DragDrop + + + Qt::CopyAction + + + true + + + + + + + + + + + + + + + + 0 + 0 + 848 + 29 + + + + + Fi&le + + + + &Recent + + + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + + Edit + + + + + + + + + + + + + + &Tools + + + + + + &View + + + + + + + + + + + + + true + + + + + + + + + + &Open ... + + + Ctrl+O + + + + + + + + + + &Quit + + + Ctrl+Q + + + + + + + + + + &About + + + + + + + + + + &Save + + + Ctrl+S + + + + + + + + + + &New ... + + + Ctrl+N + + + + + + + + + + &Close + + + Ctrl+Shift+Q + + + + + + + + + + Change &password ... + + + + + + + + + + &Add new account + + + Ctrl+Shift+A + + + + + + + + + + Sa&ve as ... + + + Ctrl+Shift+S + + + + + + + + + + Remove selected entry + + + Remove selected entry + + + + + + + + + + &Insert field + + + + + + + + + + Remove selected &field(s) + + + + + &Password generator + + + + + + + + + + &Clear list + + + + + true + + + true + + + &Always create backup + + + + + + + + + + &Export ... + + + + + + + + + + Show containing &directory + + + + + false + + + + + + + + &Undo + + + + + false + + + + + + + + &Redo + + + + + + + + + + Add &new category + + + + + true + + + &Show undo stack + + + + + true + + + &Hide passwords + + + + + + + + Widgets::ClearLineEdit + QLineEdit +
qtutilities/widgets/clearlineedit.h
+
+
+ + + + +
diff --git a/gui/passwordgeneratordialog.cpp b/gui/passwordgeneratordialog.cpp new file mode 100644 index 0000000..5b6f25b --- /dev/null +++ b/gui/passwordgeneratordialog.cpp @@ -0,0 +1,169 @@ +#include "passwordgeneratordialog.h" +#include "ui_passwordgeneratordialog.h" + +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include + +using namespace std; +using namespace Io; +using namespace Util; + +namespace QtGui { + +const char smallLetters[] = {'a','b','c','d','e','f', + 'g','h','i','j','k', + 'l','m','n','o','p', + 'q','r','s','t','u', + 'v','w','x','y','z'}; + +const char capitalLetters[] = {'A','B','C','D','E','F', + 'G','H','I','J','K', + 'L','M','N','O','P', + 'Q','R','S','T','U', + 'V','W','X','Y','Z'}; + +const char digits[] = {'0','1','2','3','4', + '5','6','7','8','9'}; + +/*! + * \class PasswordGeneratorDialog + * \brief The PasswordGeneratorDialog class provides a password generation dialog. + */ + +/*! + * \brief Constructs a new password generator dialog. + */ +PasswordGeneratorDialog::PasswordGeneratorDialog(QWidget *parent) : + QDialog(parent), + m_ui(new Ui::PasswordGeneratorDialog) +{ + m_ui->setupUi(this); +#ifdef Q_OS_WIN32 + setStyleSheet(QStringLiteral("* { font: 9pt \"Segoe UI\", \"Sans\"; } #mainFrame { border: none; background: white; } #bottomFrame { background-color: #F0F0F0; border-top: 1px solid #DFDFDF; } #mainFrame #captionNameLabel, QMessageBox QLabel, QCommandLinkButton { font-size: 12pt; color: #003399; font-weight: normal; } #atLeastOneOfEachCategoryFrame { border-top: 1px solid #eee; }")); +#else + setStyleSheet(QStringLiteral("#mainFrame #captionNameLabel { font-weight: bold; }")); +#endif + + setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + + connect(m_ui->copyPasswordCommandLinkButton, SIGNAL(clicked()), this, SLOT(copyPassword())); + connect(m_ui->generatePassowordCommandLinkButton, SIGNAL(clicked()), this, SLOT(generateNewPassword())); + connect(m_ui->useCapitalLettersCheckBox, SIGNAL(stateChanged(int)), this, SLOT(handleCheckedCategoriesChanged())); + connect(m_ui->useDigitsCheckBox, SIGNAL(stateChanged(int)), this, SLOT(handleCheckedCategoriesChanged())); + connect(m_ui->otherCharsLineEdit, SIGNAL(editingFinished()), this, SLOT(handleCheckedCategoriesChanged())); + connect(m_ui->useSmallLettersCheckBox, SIGNAL(stateChanged(int)), this, SLOT(handleCheckedCategoriesChanged())); + connect(m_ui->passwordLineEdit, SIGNAL(textChanged(QString)), this, SLOT(handlePasswordChanged())); + + handlePasswordChanged(); +} + +/*! + * \brief Destroys the dialog. + */ +PasswordGeneratorDialog::~PasswordGeneratorDialog() +{ + delete m_ui; +} + +/*! + * \brief Generates and shows a new password. + */ +void PasswordGeneratorDialog::generateNewPassword() +{ + int length = m_ui->LengthSpinBox->value(); + if(length > 0) { + if(m_charset.empty()) { + bool useSmallLetters = m_ui->useSmallLettersCheckBox->isChecked(); + bool useCapitalLetters = m_ui->useCapitalLettersCheckBox->isChecked(); + bool useDigits = m_ui->useDigitsCheckBox->isChecked(); + QString otherChars = m_ui->otherCharsLineEdit->text(); + int charsetSize = otherChars.length(); + if(useSmallLetters) { + charsetSize += sizeof(smallLetters); + } + if(useCapitalLetters) { + charsetSize += sizeof(capitalLetters); + } + if(useDigits) { + charsetSize += sizeof(digits); + } + m_charset.reserve(charsetSize); + if(useSmallLetters) { + m_charset.insert(m_charset.end(), std::begin(smallLetters), std::end(smallLetters)); + } + if(useCapitalLetters) { + m_charset.insert(m_charset.end(), std::begin(capitalLetters), std::end(capitalLetters)); + } + if(useDigits) { + m_charset.insert(m_charset.end(), std::begin(digits), std::end(digits)); + } + char charval; + foreach(QChar qchar, otherChars) { + charval = qchar.toLatin1(); + if(charval != '\x00' && charval != ' ' && std::find(m_charset.begin(), m_charset.end(), charval) == m_charset.end()) { + m_charset.push_back(charval); + } + } + } + if(!m_charset.empty()) { + try { + default_random_engine rng(m_random()); + uniform_int_distribution<> dist(0, m_charset.size() - 1); + auto randchar = [this, &dist, &rng]() { + return m_charset[dist(rng)]; + }; + string res(length, 0); + generate_n(res.begin(), length, randchar); + m_ui->passwordLineEdit->setText(QString::fromLatin1(res.c_str())); + } catch(const CryptoException &ex) { + QMessageBox::warning(this, QApplication::applicationName(), tr("Failed to generate password.\nOpenSSL error: %1").arg(QString::fromLocal8Bit(ex.what()))); + } + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("You have to select at least one checkbox.")); + } + } else { + QMessageBox::warning(this, QApplication::applicationName(), tr("The length has to be at least one.")); + } +} + +/*! + * \brief Handles when the user checked or unchecked a category. + */ +void PasswordGeneratorDialog::handleCheckedCategoriesChanged() +{ + m_ui->generatePassowordCommandLinkButton->setEnabled(m_ui->useCapitalLettersCheckBox->isChecked() + || m_ui->useDigitsCheckBox->isChecked() + || m_ui->useSmallLettersCheckBox->isChecked() + || !m_ui->otherCharsLineEdit->text().isEmpty()); + m_charset.clear(); +} + +/*! + * \brief Handles when the password changed. + */ +void PasswordGeneratorDialog::handlePasswordChanged() +{ + m_ui->copyPasswordCommandLinkButton->setEnabled(m_ui->passwordLineEdit->text().count() > 0); +} + +/*! + * \brief Copies the current password to the clipboard. + */ +void PasswordGeneratorDialog::copyPassword() +{ + QClipboard *cb = QApplication::clipboard(); + cb->setText(m_ui->passwordLineEdit->text()); +} + +} diff --git a/gui/passwordgeneratordialog.h b/gui/passwordgeneratordialog.h new file mode 100644 index 0000000..dc91779 --- /dev/null +++ b/gui/passwordgeneratordialog.h @@ -0,0 +1,38 @@ +#ifndef PASSWORDGENERATORDIALOG_H +#define PASSWORDGENERATORDIALOG_H + +#include + +#include + +#include + +namespace QtGui { + +namespace Ui { +class PasswordGeneratorDialog; +} + +class PasswordGeneratorDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PasswordGeneratorDialog(QWidget *parent = 0); + ~PasswordGeneratorDialog(); + +private slots: + void generateNewPassword(); + void handleCheckedCategoriesChanged(); + void handlePasswordChanged(); + void copyPassword(); + +private: + Ui::PasswordGeneratorDialog *m_ui; + std::vector m_charset; + Util::OpenSslRandomDevice m_random; +}; + +} + +#endif // PASSWORDGENERATORDIALOG_H diff --git a/gui/passwordgeneratordialog.ui b/gui/passwordgeneratordialog.ui new file mode 100644 index 0000000..819c8e4 --- /dev/null +++ b/gui/passwordgeneratordialog.ui @@ -0,0 +1,349 @@ + + + QtGui::PasswordGeneratorDialog + + + + 0 + 0 + 512 + 350 + + + + + 43 + 0 + + + + + 512 + 350 + + + + + 512 + 350 + + + + Password Generator + + + + :/icons/hicolor/128x128/apps/passwordmanager.png:/icons/hicolor/128x128/apps/passwordmanager.png + + + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 10 + + + 0 + + + 0 + + + 0 + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Use small letters + + + true + + + + + + + Use capital letters + + + true + + + + + + + Use digits + + + true + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 9 + + + 0 + + + 5 + + + 0 + + + 0 + + + + + Length + + + + + + + Other characters to be used + + + + + + + !"§$%&/()=?;:_'*~’#,.-+<>| + + + 50 + + + + + + + 4 + + + 256 + + + 10 + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 5 + + + 5 + + + + + <b>Password:</b> + + + + + + + + + + + + + + 16777215 + 40 + + + + Generate new password + + + + + + + + + + + 16777215 + 40 + + + + Copy password + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + Qt::Horizontal + + + + 216 + 14 + + + + + + + + + Segoe UI,Sans + 9 + 50 + false + false + + + + Close + + + + + + + + false + + + + + + + + + + + + + + closePushbutton + clicked() + QtGui::PasswordGeneratorDialog + close() + + + 464 + 278 + + + 255 + 149 + + + + + diff --git a/gui/stacksupport.cpp b/gui/stacksupport.cpp new file mode 100644 index 0000000..aed8656 --- /dev/null +++ b/gui/stacksupport.cpp @@ -0,0 +1,17 @@ +#include "stacksupport.h" + +namespace QtGui { + +/*! + * \class StackSupport + * \brief The StackSupport class is used as base class for models supporting undoing of changes via QUndoStack. + */ + +/*! + * \brief Constructs a new stack support with the specified \a undoStack. + */ +StackSupport::StackSupport(QUndoStack *undoStack) : + m_undoStack(undoStack) +{} + +} diff --git a/gui/stacksupport.h b/gui/stacksupport.h new file mode 100644 index 0000000..83863a7 --- /dev/null +++ b/gui/stacksupport.h @@ -0,0 +1,104 @@ +#ifndef QTGUI_STACKSUPPORT_H +#define QTGUI_STACKSUPPORT_H + +#include "undocommands.h" + +#include + +namespace QtGui { + +class StackAbsorper; + +class StackSupport +{ + friend class StackAbsorper; +public: + StackSupport(QUndoStack *undoStack = nullptr); + +protected: + QUndoStack *undoStack(); + bool push(CustomUndoCommand *command); + void clearUndoStack(); + +private: + QUndoStack *m_undoStack; +}; + +/*! + * \brief Returns the undo stack for the current instance. + */ +inline QUndoStack *StackSupport::undoStack() +{ + return m_undoStack; +} + +/*! + * \brief Pushes the specified custom undo \a command to the undo stack and returns whether the redo action was successful. + */ +inline bool StackSupport::push(CustomUndoCommand *command) +{ + if(m_undoStack) { + if(command->isNoop()) { + return true; // doing nothing can never fail + } else { + m_undoStack->push(command); + return command->redoResult(); + } + } + return false; +} + +/*! + * \brief Clears the undo stack. + */ +inline void StackSupport::clearUndoStack() +{ + if(m_undoStack) { + m_undoStack->clear(); + } +} + +/*! + * \brief The StackAbsorper class is used by the CustomUndoCommand class to prevent infinite recursion when pushing + * a new command to the stack. + */ +class StackAbsorper +{ +public: + StackAbsorper(StackSupport *supported); + ~StackAbsorper(); + QUndoStack *stack(); +private: + StackSupport *m_supported; + QUndoStack *m_stack; +}; + +/*! + * \brief Detaches the undo stack from the specified stack support temporary. + */ +inline StackAbsorper::StackAbsorper(StackSupport *supported) : + m_supported(supported), + m_stack(supported->m_undoStack) +{ + m_supported->m_undoStack = nullptr; +} + +/*! + * \brief Restores the undo stack of the stack support. + */ +inline StackAbsorper::~StackAbsorper() +{ + m_supported->m_undoStack = m_stack; +} + +/*! + * \brief Returns the stack for the current instance. + */ +inline QUndoStack *StackAbsorper::stack() +{ + return m_stack; +} + +} + +#endif // QTGUI_STACKSUPPORT_H diff --git a/gui/undocommands.cpp b/gui/undocommands.cpp new file mode 100644 index 0000000..5ebf17e --- /dev/null +++ b/gui/undocommands.cpp @@ -0,0 +1,458 @@ +#include "undocommands.h" +#include "stacksupport.h" +#include "model/fieldmodel.h" +#include "model/entrymodel.h" + +#include + +#include + +using namespace std; +using namespace Io; + +namespace QtGui { + +/*! + * \class CustomUndoCommand + * \brief The CustomUndoCommand class acts as base class for undo commands used within the + * models of the application. + * \sa http://qt-project.org/doc/qt-5/qundocommand.html + */ + +/*! + * \brief Constructs a new custom undo command with the specified \a stackSupport. + */ +CustomUndoCommand::CustomUndoCommand(StackSupport *stackSupport) : + m_stackSupport(stackSupport), + m_redoResult(false), + m_undoResult(true), + m_noop(false) +{} + +void CustomUndoCommand::redo() +{ + if(m_undoResult) { + StackAbsorper stackAbsorper(m_stackSupport); + m_redoResult = internalRedo(); + } +} + +void CustomUndoCommand::undo() +{ + if(m_redoResult) { + StackAbsorper stackAbsorper(m_stackSupport); + m_undoResult = internalUndo(); + } +} + +/*! + * \fn CustomUndoCommand::internalRedo() + * \brief This method is internally called to perform the redo action. + */ + +/*! + * \fn CustomUndoCommand::internalUndo() + * \brief This method is internally called to perform the undo action. + */ + +/*! + * \class FieldModelSetValueCommand + * \brief Sets the value for the specified index and role in the specified field model. + */ + +/*! + * \brief Constructs a new command. + */ +FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QModelIndex &index, const QVariant &value, int role) : + CustomUndoCommand(model), + m_account(model->accountEntry()), + m_model(model), + m_row(index.row()), + m_col(index.column()), + m_newValue(value), + m_oldValue(model->data(index, role)), + m_role(role) +{ + QString fieldName = model->index(m_row, 0, index.parent()).data().toString(); + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(m_col) { + case 0: + if(m_oldValue.toString().isEmpty()) { + setText(QApplication::translate("undocommands", "setting field name to »%1«").arg(m_newValue.toString())); + } else { + setText(QApplication::translate("undocommands", "setting field name »%1« to »%2«").arg(m_oldValue.toString(), m_newValue.toString())); + } + break; + case 1: + if(fieldName.isEmpty()) { + setText(QApplication::translate("undocommands", "setting value of empty field")); + } else { + setText(QApplication::translate("undocommands", "setting value of »%1« field").arg(fieldName)); + } + break; + } + break; + case FieldTypeRole: + setText(QApplication::translate("undocommands", "setting type of »%1« field").arg(fieldName)); + break; + default: + setText(QApplication::translate("undocommands", "setting field property in row »%1«").arg(m_row + 1)); + } + setNoop(m_oldValue == m_newValue); +} + +bool FieldModelSetValueCommand::internalRedo() +{ + m_model->setAccountEntry(m_account); + return m_model->setData(m_model->index(m_row, m_col), m_newValue, m_role); +} + +bool FieldModelSetValueCommand::internalUndo() +{ + m_model->setAccountEntry(m_account); + return m_model->setData(m_model->index(m_row, m_col), m_oldValue, m_role); +} + +/*! + * \class FieldModelInsertRowsCommand + * \brief Inserts the specified number of rows before the specified row in the specified field model. + */ + +/*! + * \brief Constructs a new command. + */ +FieldModelInsertRowsCommand::FieldModelInsertRowsCommand(FieldModel *model, int row, int count) : + CustomUndoCommand(model), + m_account(model->accountEntry()), + m_model(model), + m_row(row), + m_count(count) +{ + setText(QApplication::translate("undocommands", "insertion of %1 row(s) before row %2", 0, count).arg(count).arg(row + 1)); +} + +bool FieldModelInsertRowsCommand::internalRedo() +{ + m_model->setAccountEntry(m_account); + return m_model->insertRows(m_row, m_count, QModelIndex()); +} + +bool FieldModelInsertRowsCommand::internalUndo() +{ + m_model->setAccountEntry(m_account); + return m_model->removeRows(m_row, m_count, QModelIndex()); +} + +/*! + * \class FieldModelRemoveRowsCommand + * \brief Removes the specified number of rows at the specified row in the specified field model. + */ + +/*! + * \brief Constructs a new command. + */ +FieldModelRemoveRowsCommand::FieldModelRemoveRowsCommand(FieldModel *model, int row, int count) : + CustomUndoCommand(model), + m_account(model->accountEntry()), + m_model(model), + m_row(row), + m_count(count) +{ + if(count == 1) { + setText(QApplication::translate("undocommands", "removal of row %1", 0, count).arg(row + 1)); + } else { + setText(QApplication::translate("undocommands", "removal of the rows %1 to %2", 0, count).arg(row + 1).arg(row + count)); + } +} + +bool FieldModelRemoveRowsCommand::internalRedo() +{ + m_model->setAccountEntry(m_account); + if(m_values.isEmpty()) { + for(int row = m_row, end = m_row + m_count; row < end; ++row) { + if(const Field *field = m_model->field(row)) { + m_values << Field(*field); + } + } + } + return m_model->removeRows(m_row, m_count, QModelIndex()); +} + +bool FieldModelRemoveRowsCommand::internalUndo() +{ + m_model->setAccountEntry(m_account); + bool res = m_model->insertRows(m_row, m_count, QModelIndex()); + for(int row = m_row, end = m_row + m_count, value = 0, values = m_values.size(); row < end && value < values; ++row, ++value) { + m_model->setData(m_model->index(row, 0), QString::fromStdString(m_values.at(value).name()), Qt::EditRole); + m_model->setData(m_model->index(row, 1), QString::fromStdString(m_values.at(value).value()), Qt::EditRole); + m_model->setData(m_model->index(row, 0), static_cast(m_values.at(value).type()), FieldTypeRole); + } + return res; +} + +/*! + * \brief Stores the entry path for the specified \a model and \a index in \a res. + */ +void indexToPath(EntryModel *model, const QModelIndex &index, list &res) +{ + res.clear(); + if(Entry *entry = model->entry(index)) { + entry->path(res); + } +} + +/*! + * \brief Fetches the entry for the specified \a model and \a path. + * \remarks The \a path will be modified. To prevent this use entryFromPathCpy(). + */ +Entry *entryFromPath(EntryModel *model, list &path) +{ + if(NodeEntry *rootEntry = model->rootEntry()) { + return rootEntry->entryByPath(path); + } + return nullptr; +} + +/*! + * \brief Fetches the entry for the specified \a model and \a path. + */ +Entry *entryFromPathCpy(EntryModel *model, list path) +{ + return entryFromPath(model, path); +} + +/*! + * \class EntryModelSetValueCommand + * \brief Sets the value for the specified index and role in the specified entry model. + */ + +/*! + * \brief Constructs a new command. + */ +EntryModelSetValueCommand::EntryModelSetValueCommand(EntryModel *model, const QModelIndex &index, const QVariant &value, int role) : + CustomUndoCommand(model), + m_model(model), + m_newValue(value), + m_oldValue(model->data(index, role)), + m_role(role) +{ + indexToPath(model, index, m_path); + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + if(m_oldValue.toString().isEmpty()) { + setText(QApplication::translate("undocommands", "setting entry name to »%1«").arg(m_newValue.toString())); + } else { + setText(QApplication::translate("undocommands", "setting entry name from »%1« to »%2«").arg(m_oldValue.toString(), m_newValue.toString())); + } + break; + default: + QString name = model->data(model->index(index.row(), 0, index.parent()), Qt::DisplayRole).toString(); + if(name.isEmpty()) { + setText(QApplication::translate("undocommands", "setting property of an entry")); + } else { + setText(QApplication::translate("undocommands", "setting property of entry »%1«").arg(name)); + } + } + setNoop(m_oldValue == m_newValue); +} + +bool EntryModelSetValueCommand::internalRedo() +{ + if(Entry *entry = entryFromPath(m_model, m_path)) { + bool res = m_model->setData(m_model->index(entry), m_newValue, m_role); + m_path.clear(); + entry->path(m_path); + return res; + } + return false; +} + +bool EntryModelSetValueCommand::internalUndo() +{ + if(Entry *entry = entryFromPath(m_model, m_path)) { + bool res = m_model->setData(m_model->index(entry), m_oldValue, m_role); + m_path.clear(); + entry->path(m_path); + return res; + } + return false; +} + +/*! + * \class EntryModelInsertRowsCommand + * \brief Modifies the specified number of rows before the specified row in the specified entry model under the specified parent. + */ + +/*! + * \brief Constructs a new command. + */ +EntryModelModifyRowsCommand::EntryModelModifyRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent) : + CustomUndoCommand(model), + m_model(model), + m_row(row), + m_count(count) +{ + indexToPath(model, parent, m_parentPath); +} + +/*! + * \brief Destroys the command. + * + * Removed entries will be deleted finally. + */ +EntryModelModifyRowsCommand::~EntryModelModifyRowsCommand() +{ + qDeleteAll(m_values); +} + +/*! + * \brief Inserts the buffered entries to the model. + */ +bool EntryModelModifyRowsCommand::insert() +{ + if(Entry *parentEntry = entryFromPathCpy(m_model, m_parentPath)) { + if(m_model->insertEntries(m_row, m_model->index(parentEntry), m_values)) { + m_values.clear(); + return true; + } + } + return false; +} + +/*! + * \brief Removes the entries from the model. + * + * The entries and the model have been specified when constructing the class. + * + * The removed entries are buffered. + */ +bool EntryModelModifyRowsCommand::remove() +{ + if(Entry *parentEntry = entryFromPathCpy(m_model, m_parentPath)) { + m_values = m_model->takeEntries(m_row, m_count, m_model->index(parentEntry)); + return !m_values.isEmpty(); + } + return false; +} + +/*! + * \class EntryModelInsertRowsCommand + * \brief Inserts the specified number of rows before the specified row in the specified entry model under the specified parent. + */ + +/*! + * \brief Constructs a new command. + */ +EntryModelInsertRowsCommand::EntryModelInsertRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent) : + EntryModelModifyRowsCommand(model, row, count, parent) +{ + setText(QApplication::translate("undocommands", "insertion of %1 entry/entries", 0, count).arg(count)); + switch(m_model->insertType()) { + case EntryType::Account: + m_values << new AccountEntry; + break; + case EntryType::Node: + m_values << new NodeEntry; + break; + } +} + +bool EntryModelInsertRowsCommand::internalRedo() +{ + return insert(); +} + +bool EntryModelInsertRowsCommand::internalUndo() +{ + return remove(); +} + +/*! + * \class EntryModelRemoveRowsCommand + * \brief Removes the specified number of rows at the specified row in the specified entry model under the specified parent. + */ + +/*! + * \brief Constructs a new command. + */ +EntryModelRemoveRowsCommand::EntryModelRemoveRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent) : + EntryModelModifyRowsCommand(model, row, count, parent) +{ + setText(QApplication::translate("undocommands", "removal of %1 entry/entries", 0, count).arg(count)); +} + +bool EntryModelRemoveRowsCommand::internalRedo() +{ + return remove(); +} + +bool EntryModelRemoveRowsCommand::internalUndo() +{ + return insert(); +} + +/*! + * \class EntryModelMoveRowsCommand + * \brief Moves the specified rows to the specified destination within the specified entry model. + */ + +/*! + * \brief Constructs a new command. + */ +EntryModelMoveRowsCommand::EntryModelMoveRowsCommand(EntryModel *model, const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) : + CustomUndoCommand(model), + m_model(model), + m_sourceRow(sourceRow), + m_count(count), + m_destChild(destinationChild) +{ + indexToPath(model, sourceParent, m_sourceParentPath); + indexToPath(model, destinationParent, m_destParentPath); + setText(QApplication::translate("undocommands", "move of %1 entry/entries", 0, count).arg(count)); +} + +bool EntryModelMoveRowsCommand::internalRedo() +{ + if(m_count) { + Entry *sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath); + Entry *destParentEntry = entryFromPathCpy(m_model, m_destParentPath); + if(sourceParentEntry && destParentEntry) { + return m_model->moveRows(m_model->index(sourceParentEntry), m_sourceRow, m_count, m_model->index(destParentEntry), m_destChild); + } + return false; + } + return true; +} + +bool EntryModelMoveRowsCommand::internalUndo() +{ + if(m_count) { + Entry *sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath); + Entry *destParentEntry = entryFromPathCpy(m_model, m_destParentPath); + if(sourceParentEntry && destParentEntry) { + int sourceRow = m_destChild; + int destChild = m_sourceRow; + // moves whithin the same parent needs special consideration + if(sourceParentEntry == destParentEntry) { + // move entry down + if(m_sourceRow < m_destChild) { + sourceRow -= m_count; + // move entry up + } else if(m_sourceRow > m_destChild) { + destChild += m_count; + // keep entry were it is + } else { + return true; + } + } + return m_model->moveRows(m_model->index(destParentEntry), sourceRow, m_count, m_model->index(sourceParentEntry), destChild); + } + return false; + } + return true; +} + +} diff --git a/gui/undocommands.h b/gui/undocommands.h new file mode 100644 index 0000000..9495211 --- /dev/null +++ b/gui/undocommands.h @@ -0,0 +1,194 @@ +#ifndef UNDOCOMMANDS_H +#define UNDOCOMMANDS_H + +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QModelIndex; +QT_END_NAMESPACE + +namespace Io { +class Entry; +class AccountEntry; +} + +namespace QtGui { + +class FieldModel; +class EntryModel; +class StackSupport; + +class CustomUndoCommand : public QUndoCommand +{ +public: + explicit CustomUndoCommand(StackSupport *stackSupport); + bool redoResult() const; + bool undoResult() const; + bool isNoop() const; + void redo(); + void undo(); +protected: + void setNoop(bool noop); + virtual bool internalRedo() = 0; + virtual bool internalUndo() = 0; +private: + StackSupport *m_stackSupport; + bool m_redoResult; + bool m_undoResult; + bool m_noop; +}; + +/*! + * \brief Returns whether the redo action was successful. + */ +inline bool CustomUndoCommand::redoResult() const +{ + return m_redoResult; +} + +/*! + * \brief Returns whether the undo action was successful. + */ +inline bool CustomUndoCommand::undoResult() const +{ + return m_undoResult; +} + +/*! + * \brief Returns whether this command does nothing, eg. the new value equals the old value. + */ +inline bool CustomUndoCommand::isNoop() const +{ + return m_noop; +} + +/*! + * \brief Sets whether this command does nothing. + * + * Meant to be called in the constructor when subclassing and - for example - the new value equals the old value. + */ +inline void CustomUndoCommand::setNoop(bool noop) +{ + m_noop = noop; +} + +class FieldModelSetValueCommand : public CustomUndoCommand +{ +public: + explicit FieldModelSetValueCommand(FieldModel *model, const QModelIndex &index, const QVariant &value, int role); +protected: + bool internalRedo(); + bool internalUndo(); +private: + Io::AccountEntry *m_account; + FieldModel *m_model; + int m_row; + int m_col; + QVariant m_newValue; + QVariant m_oldValue; + int m_role; +}; + +class FieldModelInsertRowsCommand : public CustomUndoCommand +{ +public: + explicit FieldModelInsertRowsCommand(FieldModel *model, int row, int count); +protected: + bool internalRedo(); + bool internalUndo(); +private: + Io::AccountEntry *m_account; + FieldModel *m_model; + int m_row; + int m_count; +}; + +class FieldModelRemoveRowsCommand : public CustomUndoCommand +{ +public: + explicit FieldModelRemoveRowsCommand(FieldModel *model, int row, int count); +protected: + bool internalRedo(); + bool internalUndo(); +private: + Io::AccountEntry *m_account; + FieldModel *m_model; + int m_row; + int m_count; + QList m_values; +}; + +class EntryModelSetValueCommand : public CustomUndoCommand +{ +public: + explicit EntryModelSetValueCommand(EntryModel *model, const QModelIndex &index, const QVariant &value, int role); +protected: + bool internalRedo(); + bool internalUndo(); +private: + EntryModel *m_model; + std::list m_path; + QVariant m_newValue; + QVariant m_oldValue; + int m_role; +}; + +class EntryModelModifyRowsCommand : public CustomUndoCommand +{ +public: + ~EntryModelModifyRowsCommand(); +protected: + explicit EntryModelModifyRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent); + bool internalRedo() = 0; + bool internalUndo() = 0; + bool insert(); + bool remove(); + EntryModel *m_model; + std::list m_parentPath; + int m_row; + int m_count; + QList m_values; +}; + +class EntryModelInsertRowsCommand : public EntryModelModifyRowsCommand +{ +public: + explicit EntryModelInsertRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent); +protected: + bool internalRedo(); + bool internalUndo(); +}; + +class EntryModelRemoveRowsCommand : public EntryModelModifyRowsCommand +{ +public: + explicit EntryModelRemoveRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent); +protected: + bool internalRedo(); + bool internalUndo(); +}; + +class EntryModelMoveRowsCommand : public CustomUndoCommand +{ +public: + explicit EntryModelMoveRowsCommand(EntryModel *model, const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild); +protected: + bool internalRedo(); + bool internalUndo(); +private: + EntryModel *m_model; + std::list m_sourceParentPath; + int m_sourceRow; + int m_count; + std::list m_destParentPath; + int m_destChild; +}; + +} + +#endif // UNDOCOMMANDS_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..4622580 --- /dev/null +++ b/main.cpp @@ -0,0 +1,95 @@ +#include "cli/cli.h" +#ifdef GUI_QTWIDGETS +# include "gui/initiate.h" +#endif +#ifdef GUI_QTQUICK +# include "quickgui/initiate.h" +#endif + +#include + +#include +#include + +#if defined(GUI_QTWIDGETS) || defined(GUI_QTQUICK) +# include +#else +# include +#endif + +#include + +#include + +using namespace std; +using namespace ApplicationUtilities; +using namespace Util; + +int main(int argc, char *argv[]) +{ + // init open ssl + OpenSsl::init(); + // setup argument parser + ArgumentParser parser; + parser.setIgnoreUnknownArguments(true); + // Qt configuration arguments + QT_CONFIG_ARGUMENTS qtConfigArgs; + // file argument + Argument fileArg("file", "f", "specifies the file to be opened (or created when using --modify)"); + fileArg.setValueNames({"path"}); + fileArg.setRequiredValueCount(1); + fileArg.setCombinable(true); + fileArg.setRequired(false); + fileArg.setImplicit(true); + // cli argument + Argument cliArg("interactive-cli", "i", "starts the interactive command line interface"); + // help argument + HelpArgument helpArg(parser); + parser.setMainArguments({&fileArg, &helpArg, &(qtConfigArgs.qtWidgetsGuiArg()), &(qtConfigArgs.qtQuickGuiArg()), &cliArg}); + // holds the application's return code + int res = 0; + // parse the specified arguments + try { + parser.parseArgs(argc, argv); + if(cliArg.isPresent()) { + Cli::InteractiveCli cli; + if(fileArg.isPresent() && fileArg.valueCount() == 1) { + cli.run(fileArg.value(0)); + } else { + cli.run(); + } + } else if(qtConfigArgs.areQtGuiArgsPresent()) { + // run Qt gui if no arguments, --qt-gui or --qt-quick-gui specified, a file might be specified + QString file; + if(fileArg.valueCount() > 0) { + file = QString::fromLocal8Bit(fileArg.value(0).c_str()); + } + if(qtConfigArgs.qtWidgetsGuiArg().isPresent()) { +#ifdef GUI_QTWIDGETS + res = QtGui::runWidgetsGui(argc, argv, file); +#else + cout << "The application has not been built with Qt widgets support." << endl; +#endif + } else if(qtConfigArgs.qtQuickGuiArg().isPresent()) { +#ifdef GUI_QTQUICK + res = QtGui::runQuickGui(argc, argv); +#else + cout << "The application has not been built with Qt quick support." << endl; +#endif + } else { +#if defined(GUI_QTQUICK) + res = QtGui::runQuickGui(argc, argv); +#elif defined(GUI_QTWIDGETS) + res = QtGui::runWidgetsGui(argc, argv, file); +#else + cout << "See --help for usage." << endl; +#endif + } + } + } catch(Failure &ex) { + cout << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; + } + // clean open ssl + OpenSsl::clean(); + return res; +} diff --git a/model/entryfiltermodel.cpp b/model/entryfiltermodel.cpp new file mode 100644 index 0000000..cee184b --- /dev/null +++ b/model/entryfiltermodel.cpp @@ -0,0 +1,32 @@ +#include "entryfiltermodel.h" +#include "entrymodel.h" + +namespace QtGui { + +/*! + * \class EntryFilterModel + * \brief The EntryFilterModel class provides a QSortFilterProxyModel specialization for the EntryModel class. + * + * \sa http://qt-project.org/doc/qt-5/qsortfilterproxymodel.html + */ + +/*! + * \brief Constructs a new filter entry model. + */ +EntryFilterModel::EntryFilterModel(QObject *parent) : + QSortFilterProxyModel(parent) +{} + +bool EntryFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if(QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) { + return true; + } + QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); + if(sourceModel()->hasChildren(sourceIndex)) { + return true; + } + return false; +} + +} diff --git a/model/entryfiltermodel.h b/model/entryfiltermodel.h new file mode 100644 index 0000000..4c3c10b --- /dev/null +++ b/model/entryfiltermodel.h @@ -0,0 +1,20 @@ +#ifndef ENTRYFILTERMODEL_H +#define ENTRYFILTERMODEL_H + +#include + +namespace QtGui { + +class EntryFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit EntryFilterModel(QObject *parent = nullptr); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; +}; + +} + +#endif // ENTRYFILTERMODEL_H diff --git a/model/entrymodel.cpp b/model/entrymodel.cpp new file mode 100644 index 0000000..db41df6 --- /dev/null +++ b/model/entrymodel.cpp @@ -0,0 +1,563 @@ +#include "entrymodel.h" + +#ifdef MODEL_UNDO_SUPPORT +#include "gui/undocommands.h" +#endif + +#include + +#include +#include +#include +#include + +#include + +using namespace std; +using namespace Io; + +namespace QtGui { + + +/*! + * \class EntryModel + * \brief The EntryModel class provides a model interface for a hierarchy of Entry instances. + * + * If MODEL_UNDO_SUPPORT the model supports Qt's undo framework. + * \sa http://qt-project.org/doc/qt-5/qabstractitemmodel.html + * \sa http://qt-project.org/doc/qt-5/qundo.html + */ + +/*! + * \brief Constructs a new entry model. + */ +EntryModel::EntryModel(QObject *parent) : + QAbstractItemModel(parent), + m_rootEntry(nullptr), + m_insertType(EntryType::Node) +{} + +#ifdef MODEL_UNDO_SUPPORT +/*! + * \brief Constructs a new entry model with the specified \a undoStack. + * + * This constructor is only available when MODEL_UNDO_SUPPORT is defined. + */ +EntryModel::EntryModel(QUndoStack *undoStack, QObject *parent) : + QAbstractItemModel(parent), + StackSupport(undoStack), + m_rootEntry(nullptr), + m_insertType(EntryType::Node) +{} +#endif + +/*! + * \brief Returns the Entry for the specified \a index of nullptr if the \a index is invalid. + * + * Modifications should be done using the common methods defined by the QAbstractItemModel class. + * This method is intended to be used only internally to implement the QAbstractItemModel interface + * or when implementing a QUndoCommand to support Qt's undo framework. + */ +Entry *EntryModel::entry(const QModelIndex &index) +{ + if(index.isValid()) { + return static_cast(index.internalPointer()); + } else { + return nullptr; + } +} + +/*! + * \brief Removes \a count number of entries at the specified \a row within the specified \a parent. + * \returns Returns the removed entries (rather then deleting them). + * + * Modifications should be done using the common methods defined by the QAbstractItemModel class. + * This method is intended to be used only internally to implement the QAbstractItemModel interface + * or when implementing a QUndoCommand to support Qt's undo framework. + */ +QList EntryModel::takeEntries(int row, int count, const QModelIndex &parent) +{ + QList res; + if(Entry *parentEntry = entry(parent)) { + if(parentEntry->type() == EntryType::Node) { + NodeEntry *parentNodeEntry = static_cast(parentEntry); + int lastIndex = row + count - 1; + const vector &children = parentNodeEntry->children(); + if(lastIndex < 0 || static_cast(lastIndex) >= children.size()) { + lastIndex = children.size() - 1; + } + beginRemoveRows(parent, row, lastIndex); + for(int index = lastIndex; index >= row; --index) { + Entry *child = children.at(index); + child->setParent(nullptr); + res << child; + } + endRemoveRows(); + } + } + return res; +} + +/*! + * \brief Inserts the specified \a entries before the specified \a row within the specified \a parent. + * + * Modifications should be done using the common methods defined by the QAbstractItemModel class. + * This method is intended to be used only internally to implement the QAbstractItemModel interface + * or when implementing a QUndoCommand to support Qt's undo framework. + */ +bool EntryModel::insertEntries(int row, const QModelIndex &parent, const QList &entries) +{ + if(entries.isEmpty()) { + return true; + } + if(Entry *parentEntry = entry(parent)) { + if(parentEntry->type() == EntryType::Node) { + NodeEntry *parentNodeEntry = static_cast(parentEntry); + const vector &children = parentNodeEntry->children(); + if(row < 0 || static_cast(row) > children.size()) { + row = children.size(); + } + beginInsertRows(parent, row, row + entries.size() - 1); + foreach(Entry *entry, entries) { + entry->setParent(parentNodeEntry, row); + ++row; + } + endInsertRows(); + return true; + } + } + return false; +} + +QModelIndex EntryModel::index(int row, int column, const QModelIndex &parent) const +{ + if(parent.isValid()) { + if(Entry *parentEntry = static_cast(parent.internalPointer())) { + switch(parentEntry->type()) { + case EntryType::Node: { + const std::vector &children = static_cast(parentEntry)->children(); + if(row >= 0 && static_cast(row) < children.size()) { + return createIndex(row, column, children.at(row)); + } + break; + } case EntryType::Account: + ; + } + } + } else if(m_rootEntry && row == 0) { + return createIndex(row, column, m_rootEntry); + } + return QModelIndex(); +} + +/*! + * \brief Returns the index of the specified \a entry. + * \remarks It is up to the caller to ensure that \a entry is a child of the root element. + */ +QModelIndex EntryModel::index(Entry *entry) const +{ + if(entry->parent()) { + return createIndex(entry->index(), 0, entry); + } else { + return createIndex(0, 0, m_rootEntry); + } +} + +QModelIndex EntryModel::parent(const QModelIndex &child) const +{ + if(child.isValid()) { + if(Entry *entry = static_cast(child.internalPointer())) { + NodeEntry *parent = entry->parent(); + if(parent && (child.row() >= 0 && static_cast(child.row()) < parent->children().size())) { + return createIndex(parent->index() > 0 ? parent->index() : 0, 0, parent); + } + } + } + return QModelIndex(); +} + +bool EntryModel::hasChildren(const QModelIndex &parent) const +{ + if(parent.isValid()) { + if(Entry *entry = static_cast(parent.internalPointer())) { + if(entry->type() == EntryType::Node) { + return static_cast(entry)->children().size(); + } + } + } else { + return true; + } + return false; +} + +/*! + * \brief Returns an indication whether the specified \a parent might have children. + * \remarks Only node entries might have childs. + */ +bool EntryModel::isNode(const QModelIndex &parent) const +{ + if(parent.isValid()) { + if(Entry *entry = static_cast(parent.internalPointer())) { + return entry->type() == EntryType::Node; + } + } + return false; +} + +QVariant EntryModel::data(const QModelIndex &index, int role) const +{ + if(index.isValid()) { + if(Entry *entry = static_cast(index.internalPointer())) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: + return QString::fromStdString(entry->label()); + default: + ; + } + break; + case Qt::DecorationRole: + if(index.column() == 0 && entry->type() == EntryType::Node) { + static const QVariant folderIcon = QIcon::fromTheme(QStringLiteral("folder")); + return folderIcon; + } + break; + case SerializedRole: { + stringstream ss(stringstream::in | stringstream::out | stringstream::binary); + ss.exceptions(std::stringstream::failbit | std::stringstream::badbit); + try { + entry->make(ss); + string string = ss.str(); + return QByteArray(string.data(), string.size()); + } catch(const ios_base::failure &) { + return false; + } + } + break; + case DefaultExpandedRole: + return entry->type() == EntryType::Node && static_cast(entry)->isExpandedByDefault(); + default: + ; + } + } + } + return QVariant(); +} + +QMap EntryModel::itemData(const QModelIndex &index) const +{ + QMap roles; + roles.insert(Qt::DisplayRole, data(index, Qt::DisplayRole)); + roles.insert(SerializedRole, data(index, SerializedRole)); + return roles; +} + +bool EntryModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ +#ifdef MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new EntryModelSetValueCommand(this, index, value, role)); + } +#endif + if(index.isValid()) { + if(Entry *entry = static_cast(index.internalPointer())) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: + entry->setLabel(value.toString().toStdString()); + emit dataChanged(index, index, QVector() << role); + return true; + default: + ; + } + break; + case SerializedRole: { + NodeEntry *parent = entry->parent(); + QModelIndex parentIndex = index.parent(); + if(parent && parentIndex.isValid()) { + stringstream ss(stringstream::in | stringstream::out | stringstream::binary); + ss.exceptions(std::stringstream::failbit | std::stringstream::badbit); + QByteArray array = value.toByteArray(); + if(array.size()) { + try { + ss.write(array.data(), array.size()); + Entry *newEntry = Entry::parse(ss); + int row = entry->index(); + beginRemoveRows(parentIndex, row, row); + delete entry; + endRemoveRows(); + beginInsertRows(parentIndex, row, row); + newEntry->setParent(parent, row); + endInsertRows(); + return true; + } catch(const ios_base::failure &) {} + } + } + } + break; + case DefaultExpandedRole: + switch(entry->type()) { + case EntryType::Account: + return false; + case EntryType::Node: + static_cast(entry)->setExpandedByDefault(value.toBool()); + emit dataChanged(index, index, QVector() << role); + return true; + } + break; + default: + ; + } + } + } + return false; +} + +bool EntryModel::setItemData(const QModelIndex &index, const QMap &roles) +{ + for(QMap::ConstIterator it = roles.constBegin(); it != roles.constEnd(); ++it) { + setData(index, it.value(), it.key()); + } + return true; +} + +Qt::ItemFlags EntryModel::flags(const QModelIndex &index) const +{ + if(isNode(index)) { + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; + } else { + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; + } +} + +QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) { + case Qt::Horizontal: + switch(role) { + case Qt::DisplayRole: + switch(section) { + case 0: + return tr("Name"); + default: + ; + } + break; + default: + ; + } + break; + default: + ; + } + return QVariant(); + return QVariant(); +} + +int EntryModel::rowCount(const QModelIndex &parent) const +{ + if(parent.isValid()) { + if(Entry *parentEntry = static_cast(parent.internalPointer())) { + switch(parentEntry->type()) { + case EntryType::Node: + return static_cast(parentEntry)->children().size(); + case EntryType::Account: + ; + } + } + } else if(m_rootEntry) { + return 1; + } + return 0; +} + +int EntryModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +bool EntryModel::insertRows(int row, int count, const QModelIndex &parent) +{ +#ifdef MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new EntryModelInsertRowsCommand(this, row, count, parent)); + } +#endif + if(parent.isValid()) { + if(Entry *parentEntry = static_cast(parent.internalPointer())) { + if(parentEntry->type() == EntryType::Node) { + beginInsertRows(parent, row, row + count - 1); + for(int end = row + count; row < end; ++row) { + Entry *newEntry; + switch(m_insertType) { + case EntryType::Node: + newEntry = new NodeEntry; + break; + case EntryType::Account: + newEntry = new AccountEntry; + break; + default: + return false; // should never be reached, just to suppress compiler warning + } + newEntry->setParent(static_cast(parentEntry), row); + } + endInsertRows(); + return true; + } + } + } + return false; +} + +bool EntryModel::removeRows(int row, int count, const QModelIndex &parent) +{ +#ifdef MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new EntryModelRemoveRowsCommand(this, row, count, parent)); + } +#endif + if(parent.isValid() && count > 0) { + if(Entry *parentEntry = static_cast(parent.internalPointer())) { + if(parentEntry->type() == EntryType::Node) { + beginRemoveRows(parent, row, row + count - 1); + static_cast(parentEntry)->deleteChildren(row, row + count); + endRemoveRows(); + return true; + } + } + } + return false; +} + +bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) +{ +#ifdef MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new EntryModelMoveRowsCommand(this, sourceParent, sourceRow, count, destinationParent, destinationChild)); + } +#endif + // check validation of specified arguments + if(sourceParent.isValid() && destinationParent.isValid() + && sourceRow >= 0 && count > 0 + && entry(sourceParent)->type() == EntryType::Node // source and destination parent entries + && entry(destinationParent)->type() == EntryType::Node) { // need to be node entries + // determine the source parent entry and dest parent entry as node entries + NodeEntry *srcParentEntry = static_cast(sourceParent.internalPointer()); + NodeEntry *destParentEntry = static_cast(destinationParent.internalPointer()); + // source rows must be within the valid range + if(static_cast(sourceRow + count) <= srcParentEntry->children().size() + // if source and destination parent are the same the destination child mustn't be in the source range + && (srcParentEntry != destParentEntry || (destinationChild < sourceRow || (sourceRow + count) < destinationChild))) { + // do not move a row to one of its own children! -> check before + for(int index = 0; index < count; ++index) { + Entry *toMove = srcParentEntry->children().at(sourceRow + index); + if(toMove->type() == EntryType::Node) { + if(destParentEntry->isIndirectChildOf(static_cast(toMove))) { + return false; + } + } + } + // actually perform the move operation + beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild); + for(int index = 0; index < count; ++index) { + Entry *toMove = srcParentEntry->children().at(sourceRow + index); + if(srcParentEntry == destParentEntry && sourceRow < destinationChild) { + toMove->setParent(destParentEntry, destinationChild + index - 1); + } else { + toMove->setParent(destParentEntry, destinationChild + index); + } + } + endMoveRows(); + return true; + } + } + return false; +} + +QStringList EntryModel::mimeTypes() const +{ + return QStringList() << QStringLiteral("application/x-entrymodelpathlistmove") << QStringLiteral("text/plain"); +} + +QMimeData *EntryModel::mimeData(const QModelIndexList &indexes) const +{ + if(indexes.count() <= 0) { + return nullptr; + } + QStringList types = mimeTypes(); + if(types.isEmpty()) { + return nullptr; + } + QMimeData *data = new QMimeData(); + QStringList plainTextParts; + QByteArray encoded; + QDataStream dataStream(&encoded, QIODevice::WriteOnly); + foreach(const QModelIndex &index, indexes) { + if(index.isValid()) { + Entry *entry = static_cast(index.internalPointer()); + list path = entry->path(); + dataStream << static_cast(path.size()); + for(const string &part : path) { + dataStream << QString::fromStdString(part); + } + plainTextParts << QString::fromStdString(entry->label()); + } + } + data->setData(types.at(0), encoded); + data->setText(plainTextParts.join(QStringLiteral(", "))); + return data; +} + +bool EntryModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + if(!m_rootEntry || !data || action != Qt::MoveAction) { + return false; + } + QStringList types = mimeTypes(); + if(types.isEmpty()) { + return false; + } + QString format = types.at(0); + if(!data->hasFormat(format)) { + return false; + } + if(row > rowCount(parent) || row < 0) { + row = rowCount(parent); + } + if(column > columnCount(parent) || column < 0) { + column = 0; + } + // decode and insert + QByteArray encoded = data->data(format); + QDataStream stream(&encoded, QIODevice::ReadOnly); + int moved = 0; + while(!stream.atEnd()) { + quint32 size; + stream >> size; + list path; + for(quint32 i = 0; i < size; ++i) { + QString part; + stream >> part; + path.push_back(part.toStdString()); + } + if(Entry *entry = m_rootEntry->entryByPath(path, true)) { + if(NodeEntry *srcParentEntry = entry->parent()) { + if(moveRows(index(srcParentEntry), entry->index(), 1, parent, row)) { + ++moved; + } + } + } + } + return false; +} + +Qt::DropActions EntryModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +} diff --git a/model/entrymodel.h b/model/entrymodel.h new file mode 100644 index 0000000..deb0fa0 --- /dev/null +++ b/model/entrymodel.h @@ -0,0 +1,127 @@ +#ifndef ENTRYMODEL_H +#define ENTRYMODEL_H + +#ifdef MODEL_UNDO_SUPPORT +#include "gui/stacksupport.h" +#endif + +#include + +#include + +namespace Io { +class Entry; +class NodeEntry; +DECLARE_ENUM(EntryType, int) +} + +namespace QtGui { + +/*! + * \brief The EntryModelRoles enum defines custom roles for the EntryModel class. + */ +enum EntryModelRoles +{ + SerializedRole = Qt::UserRole + 1, /**< the entry (including descendants) in serialized from (QByteArray) */ + DefaultExpandedRole = Qt::UserRole + 2 /**< whether the entry should be expanded by default */ +}; + +class EntryModel : public QAbstractItemModel +#ifdef MODEL_UNDO_SUPPORT + , public StackSupport +#endif +{ + Q_OBJECT +public: + explicit EntryModel(QObject *parent = nullptr); +#ifdef MODEL_UNDO_SUPPORT + explicit EntryModel(QUndoStack *undoStack, QObject *parent = nullptr); +#endif + + Io::NodeEntry *rootEntry(); + void setRootEntry(Io::NodeEntry *entry); + Io::Entry *entry(const QModelIndex &index); + QList takeEntries(int row, int count, const QModelIndex &parent); + bool insertEntries(int row, const QModelIndex &parent, const QList &entries); + Io::EntryType insertType() const; + void setInsertType(Io::EntryType type); + bool hidePasswords() const; + QModelIndex index(int row, int column, const QModelIndex &parent) const; + QModelIndex index(Io::Entry *entry) const; + QModelIndex parent(const QModelIndex &child) const; + bool hasChildren(const QModelIndex &parent) const; + bool isNode(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + QMap itemData(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + bool setItemData(const QModelIndex &index, const QMap &roles); + Qt::ItemFlags flags(const QModelIndex &index) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + bool insertRows(int row, int count, const QModelIndex &parent); + bool removeRows(int row, int count, const QModelIndex &parent); + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild); + QStringList mimeTypes() const; + QMimeData *mimeData(const QModelIndexList &indexes) const; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + Qt::DropActions supportedDropActions() const; + +public slots: + void reset(); + +private: + Io::NodeEntry *m_rootEntry; + Io::EntryType m_insertType; +}; + +/*! + * \brief Returns the root entry. + */ +inline Io::NodeEntry *EntryModel::rootEntry() +{ + return m_rootEntry; +} + +/*! + * \brief Sets the root entry. Causes a model reset. The undo stack will be cleard if MODEL_UNDO_SUPPORT is defined. + */ +inline void EntryModel::setRootEntry(Io::NodeEntry *entry) +{ + if(m_rootEntry != entry) { +#ifdef MODEL_UNDO_SUPPORT + clearUndoStack(); +#endif + beginResetModel(); + m_rootEntry = entry; + endResetModel(); + } +} + +/*! + * \brief Resets the model. The root entry will be unset. + */ +inline void EntryModel::reset() +{ + setRootEntry(nullptr); +} + +/*! + * \brief Returns the entry type used when inserting new rows. + */ +inline Io::EntryType EntryModel::insertType() const +{ + return m_insertType; +} + +/*! + * \brief Sets the entry type used when inserting new rows. + */ +inline void EntryModel::setInsertType(Io::EntryType type) +{ + m_insertType = type; +} + +} + +#endif // ENTRYMODEL_H diff --git a/model/fieldmodel.cpp b/model/fieldmodel.cpp new file mode 100644 index 0000000..a749513 --- /dev/null +++ b/model/fieldmodel.cpp @@ -0,0 +1,339 @@ +#include "fieldmodel.h" +#ifdef MODEL_UNDO_SUPPORT +#include "gui/undocommands.h" +#endif + +#include + +#include +#include + +using namespace std; +using namespace Io; + +namespace QtGui { + +/*! + * \class FieldModel + * \brief The FieldModel class provides a model interface for the fields of an AccountEntry. + * + * If MODEL_UNDO_SUPPORT the model supports Qt's undo framework. + * \sa http://qt-project.org/doc/qt-5/qabstracttablemodel.html + * \sa http://qt-project.org/doc/qt-5/qundo.html + */ + +/*! + * \brief Constructs a new field model. + */ +FieldModel::FieldModel(QObject *parent) : + QAbstractTableModel(parent), + m_accountEntry(nullptr), + m_fields(nullptr), + m_hidePasswords(false) +{} + +#ifdef MODEL_UNDO_SUPPORT +/*! + * \brief Constructs a new field model with the specified \a undoStack. + * + * This constructor is only available when MODEL_UNDO_SUPPORT is defined. + */ +FieldModel::FieldModel(QUndoStack *undoStack, QObject *parent) : + QAbstractTableModel(parent), + StackSupport(undoStack), + m_accountEntry(nullptr), + m_fields(nullptr) +{} +#endif + +/*! + * \brief Sets the account entry. Causes a model reset. + * + * The \a entry might be nullptr. + * The caller keeps the ownership and should not destroy the \a entry as long it is assigned. + */ +void FieldModel::setAccountEntry(AccountEntry *entry) +{ + if(entry != m_accountEntry) { + beginResetModel(); + if((m_accountEntry = entry)) { + m_fields = &entry->fields(); + } else { + m_fields = nullptr; + } + endResetModel(); + } +} + +QVariant FieldModel::data(const QModelIndex &index, int role) const +{ + if(index.isValid() && m_fields && index.row() >= 0) { + // return data for existent field + if(static_cast(index.row()) < m_fields->size()) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: + return QString::fromStdString(m_fields->at(index.row()).name()); + case 1: + if(role == Qt::DisplayRole + && m_hidePasswords + && m_fields->at(index.row()).type() == FieldType::Password) { + return QString(m_fields->at(index.row()).value().size(), QChar(0x2022)); + } else { + return QString::fromStdString(m_fields->at(index.row()).value()); + } + default: + ; + } + break; + case FieldTypeRole: + return static_cast(m_fields->at(index.row()).type()); + default: + ; + } + // return data for empty field at the end which enables the user to append fields + } else if(static_cast(index.row()) == m_fields->size()) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: + return QString(); + case 1: + return QString(); + default: + ; + } + break; + default: + ; + } + } + } + return QVariant(); +} + +QMap FieldModel::itemData(const QModelIndex &index) const +{ + static int roles[] = {Qt::EditRole, FieldTypeRole}; + QMap roleMap; + for(int role : roles) { + QVariant variantData = data(index, role); + if (variantData.isValid()) { + roleMap.insert(role, variantData); + } + } + return roleMap; +} + +bool FieldModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ +#if MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new FieldModelSetValueCommand(this, index, value, role)); + } +#endif + QVector roles; + if(index.isValid() && m_fields && index.row() >= 0) { + // set data for existing field + if(static_cast(index.row()) < m_fields->size()) { + switch(role) { + case Qt::EditRole: + switch(index.column()) { + case 0: + m_fields->at(index.row()).setName(value.toString().toStdString()); + roles << role; + break; + case 1: + m_fields->at(index.row()).setValue(value.toString().toStdString()); + roles << role; + break; + default: + ; + } + break; + case FieldTypeRole: { + bool ok; + int fieldType = value.toInt(&ok); + if(ok && Field::isValidType(fieldType)) { + roles << role; + m_fields->at(index.row()).setType(static_cast(fieldType)); + } + break; + } default: + ; + } + // remove last field if empty, showing an empty field at the end to enabled appending new rows is provided by the data method + if(!roles.isEmpty() && static_cast(index.row()) == m_fields->size() - 1 && m_fields->at(index.row()).isEmpty()) { + beginRemoveRows(index.parent(), index.row(), index.row()); + m_fields->pop_back(); + endRemoveRows(); + } + // set data for a new field emplaced at the end of the field list + } else if(static_cast(index.row()) == m_fields->size() && !value.toString().isEmpty()) { + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: + beginInsertRows(index.parent(), rowCount(), rowCount()); + m_fields->emplace_back(m_accountEntry); + m_fields->back().setName(value.toString().toStdString()); + endInsertRows(); + roles << role; + break; + case 1: + beginInsertRows(index.parent(), rowCount(), rowCount()); + m_fields->emplace_back(m_accountEntry); + m_fields->back().setValue(value.toString().toStdString()); + endInsertRows(); + roles << role; + break; + default: + ; + } + break; + default: + ; + } + } + } + // return false if nothing could be changed + if(roles.isEmpty()) { + return false; + } else { + // some roles affect other roles + switch(role) { + case Qt::EditRole: + roles << Qt::DisplayRole; + break; + case FieldTypeRole: + roles << Qt::DisplayRole << Qt::EditRole; + break; + default: + ; + } + } + // emit data changed signal on sucess + emit dataChanged(index, index, roles); + return true; +} + +Qt::ItemFlags FieldModel::flags(const QModelIndex &index) const +{ + return QAbstractTableModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; +} + +QVariant FieldModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) { + case Qt::Horizontal: + switch(role) { + case Qt::DisplayRole: + switch(section) { + case 0: + return tr("Name"); + case 1: + return tr("Value"); + default: + ; + } + break; + default: + ; + } + break; + default: + ; + } + return QVariant(); +} + +int FieldModel::rowCount(const QModelIndex &parent) const +{ + return (!parent.isValid() && m_fields) ? m_fields->size() + 1 : 0; +} + +int FieldModel::columnCount(const QModelIndex &parent) const +{ + return !parent.isValid() ? 2 : 0; +} + +bool FieldModel::insertRows(int row, int count, const QModelIndex &parent) +{ +#ifdef MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new FieldModelInsertRowsCommand(this, row, count)); + } +#endif + if(!parent.isValid() && row >= 0 && count > 0 && static_cast(row) <= m_fields->size()) { + beginInsertRows(parent, row, row + count - 1); + m_fields->insert(m_fields->begin() + row, count, Field(m_accountEntry)); + endInsertRows(); + return true; + } + return false; +} + +bool FieldModel::removeRows(int row, int count, const QModelIndex &parent) +{ +#ifdef MODEL_UNDO_SUPPORT + if(undoStack()) { + return push(new FieldModelRemoveRowsCommand(this, row, count)); + } +#endif + if(!parent.isValid() && row >= 0 && count > 0 && static_cast(row + count) <= m_fields->size()) { + beginRemoveRows(parent, row, row + count - 1); + m_fields->erase(m_fields->begin() + row, m_fields->begin() + row + count); + endRemoveRows(); + return true; + } + return false; +} + +bool FieldModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + if(!QAbstractTableModel::dropMimeData(data, action, row, column, parent)) { + if(data->hasText()) { + return setData(parent, data->text(), Qt::EditRole); + } + } + return false; +} + +QStringList FieldModel::mimeTypes() const +{ + return QAbstractTableModel::mimeTypes() << QStringLiteral("text/plain"); +} + +QMimeData *FieldModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = QAbstractTableModel::mimeData(indexes); + if(!indexes.isEmpty()) { + QStringList result; + foreach(const QModelIndex &index, indexes) { + result << index.data(Qt::EditRole).toString(); + } + data->setText(result.join(QChar('\n'))); + } + return data; +} + +/*! + * \brief Returns the field for the specified row. + * + * Might be nullptr if no account entry is assigned or if the row is out of range. + */ +const Field *FieldModel::field(size_t row) const +{ + if(m_fields && row < m_fields->size()) { + return &m_fields->at(row); + } + return nullptr; +} + +} + + diff --git a/model/fieldmodel.h b/model/fieldmodel.h new file mode 100644 index 0000000..1e93c49 --- /dev/null +++ b/model/fieldmodel.h @@ -0,0 +1,121 @@ +#ifndef FIELDMODEL_H +#define FIELDMODEL_H + +#include +#ifdef MODEL_UNDO_SUPPORT +#include "gui/stacksupport.h" +#endif + +#include + +#include + +namespace Io { +class Field; +} + +namespace QtGui { + +/*! + * \brief The FieldModelRoles enum defines custom roles for the FieldModel class. + */ +enum FieldModelRoles +{ + FieldTypeRole = Qt::UserRole + 1 /**< the field type */ +}; + +class FieldModel : public QAbstractTableModel +#ifdef MODEL_UNDO_SUPPORT + , public StackSupport +#endif +{ + Q_OBJECT +public: + explicit FieldModel(QObject *parent = nullptr); +#ifdef MODEL_UNDO_SUPPORT + explicit FieldModel(QUndoStack *undoStack, QObject *parent = nullptr); +#endif + + Io::AccountEntry *accountEntry(); + const Io::AccountEntry *accountEntry() const; + void setAccountEntry(Io::AccountEntry *entry); + std::vector *fields(); + bool hidePasswords() const; + QVariant data(const QModelIndex &index, int role) const; + QMap itemData(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + Qt::ItemFlags flags(const QModelIndex &index) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + bool insertRows(int row, int count, const QModelIndex &parent); + bool removeRows(int row, int count, const QModelIndex &parent); + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + QStringList mimeTypes() const; + QMimeData *mimeData(const QModelIndexList &indexes) const; + const Io::Field *field(std::size_t row) const; + +public slots: + void setHidePasswords(bool hidePasswords); + void reset(); + +private: + Io::AccountEntry *m_accountEntry; + std::vector *m_fields; + bool m_hidePasswords; +}; + +/*! + * \brief Returns the account entry. Might be nullptr if no account entry has been set. + * + * The ownership remains unaltered. + */ +inline Io::AccountEntry *FieldModel::accountEntry() +{ + return m_accountEntry; +} + +/*! + * \brief Returns the account entry. Might be nullptr if no account entry has been set. + * + * The ownership remains unaltered. + */ +inline const Io::AccountEntry *FieldModel::accountEntry() const +{ + return m_accountEntry; +} + +/*! + * \brief Returns the fields of the account entry. Might be nullptr if no account entry is assigned. + * + * The ownership remains unaltered. + */ +inline std::vector *FieldModel::fields() +{ + return m_fields; +} + +/*! + * \brief Resets the model. The account entry will be unset. + */ +inline void FieldModel::reset() +{ + setAccountEntry(nullptr); +} + +inline bool FieldModel::hidePasswords() const +{ + return m_hidePasswords; +} + +inline void FieldModel::setHidePasswords(bool hidePasswords) +{ + m_hidePasswords = hidePasswords; + if(m_fields) { + emit dataChanged(index(0, 1), index(m_fields->size() - 1, 1), QVector() << Qt::DisplayRole); + } +} + +} + +#endif // FIELDMODEL_H diff --git a/passwordmanager.doxygen b/passwordmanager.doxygen new file mode 100644 index 0000000..c51c16b --- /dev/null +++ b/passwordmanager.doxygen @@ -0,0 +1,2310 @@ +# Doxyfile 1.8.7 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Password Manager" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 1 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "A simple password store using AES-256-CBC encryption via OpenSSL." + +# With the PROJECT_LOGO tag one can specify an logo or icon that is included in +# the documentation. The maximum height of the logo should not exceed 55 pixels +# and the maximum width should not exceed 200 pixels. Doxygen will copy the logo +# to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = ./doc + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = YES + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce a +# new page for each member. If set to NO, the documentation of a member will be +# part of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by by putting a % sign in front of the word +# or globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO these classes will be included in the various overviews. This option has +# no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable ( YES) or disable ( NO) the +# todo list. This list is created by putting \todo commands in the +# documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable ( YES) or disable ( NO) the +# test list. This list is created by putting \test commands in the +# documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable ( YES) or disable ( NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable ( YES) or disable ( NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES the list +# will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. Do not use file names with spaces, bibtex cannot handle them. See +# also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES, then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO doxygen will only warn about wrong or incomplete parameter +# documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. +# Note: If this tag is empty the current directory is searched. + +INPUT = gui io model + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank the +# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, +# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, +# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, +# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, +# *.qsf, *.as and *.js. + +FILE_PATTERNS = *.cpp *.h + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER ) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES, then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user- +# defined cascading style sheet that is included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefor more robust against future updates. +# Doxygen will copy the style sheet file to the output directory. For an example +# see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the stylesheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler ( hhc.exe). If non-empty +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated ( +# YES) or that it should be included in the master .chm file ( NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated ( +# YES) or a normal table of contents ( NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using prerendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /