summaryrefslogtreecommitdiff
path: root/plugins/HistoryStats/src
diff options
context:
space:
mode:
authorGeorge Hazan <george.hazan@gmail.com>2014-03-04 23:23:45 +0000
committerGeorge Hazan <george.hazan@gmail.com>2014-03-04 23:23:45 +0000
commit15855fa84a09fd1fd486d357c38db0f2bd181e74 (patch)
treec98d1ae74abca41c776522059343eda13a7e0a31 /plugins/HistoryStats/src
parent819f8efabea6b0d7d971f9e6e4b21bbc18838874 (diff)
HistoryStats compiles ok now
git-svn-id: http://svn.miranda-ng.org/main/trunk@8399 1316c22d-e87f-b044-9b9b-93d7a3e3ba9c
Diffstat (limited to 'plugins/HistoryStats/src')
-rw-r--r--plugins/HistoryStats/src/_consts.cpp5
-rw-r--r--plugins/HistoryStats/src/_consts.h213
-rw-r--r--plugins/HistoryStats/src/_format.h176
-rw-r--r--plugins/HistoryStats/src/_globals.cpp1
-rw-r--r--plugins/HistoryStats/src/_globals.h92
-rw-r--r--plugins/HistoryStats/src/_langext.h35
-rw-r--r--plugins/HistoryStats/src/_strfunc.h42
-rw-r--r--plugins/HistoryStats/src/bandctrl.cpp73
-rw-r--r--plugins/HistoryStats/src/bandctrl.h40
-rw-r--r--plugins/HistoryStats/src/bandctrldefs.h59
-rw-r--r--plugins/HistoryStats/src/bandctrlimpl.cpp1057
-rw-r--r--plugins/HistoryStats/src/bandctrlimpl.h92
-rw-r--r--plugins/HistoryStats/src/canvas.cpp212
-rw-r--r--plugins/HistoryStats/src/canvas.h57
-rw-r--r--plugins/HistoryStats/src/colbase_words.cpp368
-rw-r--r--plugins/HistoryStats/src/colbase_words.h60
-rw-r--r--plugins/HistoryStats/src/column.cpp181
-rw-r--r--plugins/HistoryStats/src/column.h391
-rw-r--r--plugins/HistoryStats/src/column_chatduration.cpp257
-rw-r--r--plugins/HistoryStats/src/column_chatduration.h46
-rw-r--r--plugins/HistoryStats/src/column_events.cpp92
-rw-r--r--plugins/HistoryStats/src/column_events.h36
-rw-r--r--plugins/HistoryStats/src/column_group.cpp31
-rw-r--r--plugins/HistoryStats/src/column_group.h23
-rw-r--r--plugins/HistoryStats/src/column_inout.cpp167
-rw-r--r--plugins/HistoryStats/src/column_inout.h40
-rw-r--r--plugins/HistoryStats/src/column_inoutgraph.cpp356
-rw-r--r--plugins/HistoryStats/src/column_inoutgraph.h54
-rw-r--r--plugins/HistoryStats/src/column_nick.cpp114
-rw-r--r--plugins/HistoryStats/src/column_nick.h38
-rw-r--r--plugins/HistoryStats/src/column_protocol.cpp26
-rw-r--r--plugins/HistoryStats/src/column_protocol.h24
-rw-r--r--plugins/HistoryStats/src/column_rank.cpp33
-rw-r--r--plugins/HistoryStats/src/column_rank.h27
-rw-r--r--plugins/HistoryStats/src/column_split.cpp463
-rw-r--r--plugins/HistoryStats/src/column_split.h74
-rw-r--r--plugins/HistoryStats/src/column_splittimeline.cpp638
-rw-r--r--plugins/HistoryStats/src/column_splittimeline.h101
-rw-r--r--plugins/HistoryStats/src/column_timeline.cpp536
-rw-r--r--plugins/HistoryStats/src/column_timeline.h77
-rw-r--r--plugins/HistoryStats/src/column_wordcount.cpp202
-rw-r--r--plugins/HistoryStats/src/column_wordcount.h39
-rw-r--r--plugins/HistoryStats/src/column_words.cpp300
-rw-r--r--plugins/HistoryStats/src/column_words.h83
-rw-r--r--plugins/HistoryStats/src/contact.cpp166
-rw-r--r--plugins/HistoryStats/src/contact.h226
-rw-r--r--plugins/HistoryStats/src/dlgconfigure.cpp232
-rw-r--r--plugins/HistoryStats/src/dlgconfigure.h41
-rw-r--r--plugins/HistoryStats/src/dlgfilterwords.cpp387
-rw-r--r--plugins/HistoryStats/src/dlgfilterwords.h66
-rw-r--r--plugins/HistoryStats/src/dlgoption.cpp399
-rw-r--r--plugins/HistoryStats/src/dlgoption.h353
-rw-r--r--plugins/HistoryStats/src/dlgoption_subbase.cpp76
-rw-r--r--plugins/HistoryStats/src/dlgoption_subcolumns.cpp832
-rw-r--r--plugins/HistoryStats/src/dlgoption_subexclude.cpp339
-rw-r--r--plugins/HistoryStats/src/dlgoption_subglobal.cpp458
-rw-r--r--plugins/HistoryStats/src/dlgoption_subinput.cpp202
-rw-r--r--plugins/HistoryStats/src/dlgoption_suboutput.cpp350
-rw-r--r--plugins/HistoryStats/src/iconlib.cpp149
-rw-r--r--plugins/HistoryStats/src/iconlib.h74
-rw-r--r--plugins/HistoryStats/src/inout.h50
-rw-r--r--plugins/HistoryStats/src/main.cpp549
-rw-r--r--plugins/HistoryStats/src/main.h33
-rw-r--r--plugins/HistoryStats/src/message.cpp334
-rw-r--r--plugins/HistoryStats/src/message.h121
-rw-r--r--plugins/HistoryStats/src/mirandacontact.cpp383
-rw-r--r--plugins/HistoryStats/src/mirandacontact.h143
-rw-r--r--plugins/HistoryStats/src/mirandahistory.cpp226
-rw-r--r--plugins/HistoryStats/src/mirandahistory.h41
-rw-r--r--plugins/HistoryStats/src/mirandasettings.cpp143
-rw-r--r--plugins/HistoryStats/src/mirandasettings.h63
-rw-r--r--plugins/HistoryStats/src/mu_common.cpp527
-rw-r--r--plugins/HistoryStats/src/mu_common.h291
-rw-r--r--plugins/HistoryStats/src/optionsctrl.cpp306
-rw-r--r--plugins/HistoryStats/src/optionsctrl.h89
-rw-r--r--plugins/HistoryStats/src/optionsctrldefs.h160
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl.cpp1428
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl.h447
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_button.cpp125
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_check.cpp59
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_color.cpp163
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_combo.cpp226
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_datetime.cpp429
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_edit.cpp190
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_group.cpp39
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_item.cpp37
-rw-r--r--plugins/HistoryStats/src/optionsctrlimpl_radio.cpp125
-rw-r--r--plugins/HistoryStats/src/protocol.cpp18
-rw-r--r--plugins/HistoryStats/src/protocol.h18
-rw-r--r--plugins/HistoryStats/src/resource.h86
-rw-r--r--plugins/HistoryStats/src/settings.cpp646
-rw-r--r--plugins/HistoryStats/src/settings.h311
-rw-r--r--plugins/HistoryStats/src/settingsserializer.cpp512
-rw-r--r--plugins/HistoryStats/src/settingsserializer.h41
-rw-r--r--plugins/HistoryStats/src/settingstree.cpp260
-rw-r--r--plugins/HistoryStats/src/settingstree.h49
-rw-r--r--plugins/HistoryStats/src/statistic.cpp1676
-rw-r--r--plugins/HistoryStats/src/statistic.h210
-rw-r--r--plugins/HistoryStats/src/themeapi.cpp53
-rw-r--r--plugins/HistoryStats/src/themeapi.h50
-rw-r--r--plugins/HistoryStats/src/utf8buffer.h119
-rw-r--r--plugins/HistoryStats/src/utils.cpp1206
-rw-r--r--plugins/HistoryStats/src/utils.h179
-rw-r--r--plugins/HistoryStats/src/utils/pattern.h39
104 files changed, 23581 insertions, 0 deletions
diff --git a/plugins/HistoryStats/src/_consts.cpp b/plugins/HistoryStats/src/_consts.cpp
new file mode 100644
index 0000000000..ed401cbd2a
--- /dev/null
+++ b/plugins/HistoryStats/src/_consts.cpp
@@ -0,0 +1,5 @@
+#include "_globals.h"
+
+#define HISTORYSTATS_CONST_DEFINE
+#include "_consts.h"
+#undef HISTORYSTATS_CONST_DEFINE \ No newline at end of file
diff --git a/plugins/HistoryStats/src/_consts.h b/plugins/HistoryStats/src/_consts.h
new file mode 100644
index 0000000000..d805314873
--- /dev/null
+++ b/plugins/HistoryStats/src/_consts.h
@@ -0,0 +1,213 @@
+#if !defined(HISTORYSTATS_GUARD__CONSTS_H)
+#define HISTORYSTATS_GUARD__CONSTS_H
+
+#include "_globals.h"
+
+#if defined(HISTORYSTATS_CONST_DEFINE)
+#define CONST_T(nam, txt) extern const mu_text* nam = muT(txt);
+#define CONST_A(nam, txt) extern const mu_ansi* nam = muA(txt);
+#define CONST_W(nam, txt) extern const mu_wide* nam = muW(txt);
+#else
+#define CONST_T(nam, txt) extern const mu_text* nam;
+#define CONST_A(nam, txt) extern const mu_ansi* nam;
+#define CONST_W(nam, txt) extern const mu_wide* nam;
+#endif
+
+/*
+ * base colors:
+ *
+ * outgoing - #FF0000 (red)
+ * incoming - #007F00 (green)
+ * bar - #0000FF (blue)
+ * i/o line - #BFBFBF (lt grey)
+ * bar line - #00007F (dk blue)
+ * bar back - #BFBFBF (lt grey)
+ *
+ * base colors (a0x):
+ *
+ * outgoing - #E8641B (red)
+ * incoming - #7CC623 (green)
+ * bar - #0581B4 (blue)
+ * i/o line - #BFBFBF (lt grey)
+ * bar line - #023F59 (dk blue)
+ * bar back - #BFBFBF (lt grey)
+ */
+
+namespace con
+{
+ // protocol names
+ CONST_A(ProtoUnknown , "(Unknown protocol)" )
+
+ // historystats settings
+ CONST_A(ModHistoryStats , "HistoryStats" )
+ CONST_A(SettVersion , "_Version" )
+ CONST_A(SettLastPage , "_LastPage" )
+ CONST_A(SettShowColumnInfo , "_ShowColumnInfo" )
+ CONST_A(SettShowSupportInfo , "_ShowSupportInfo" )
+ CONST_A(SettLastStatisticsFile , "_LastStatisticsFile" )
+ CONST_A(SettAutoOpenOptions , "AutoOpenOptions" )
+ CONST_A(SettAutoOpenStartup , "AutoOpenStartup" )
+ CONST_A(SettAutoOpenMenu , "AutoOpenMenu" )
+ CONST_A(SettAverageMinTime , "AverageMinTime" )
+ CONST_A(SettCalcTotals , "CalcTotals" )
+ CONST_A(SettChatSessionMinDur , "ChatSessionMinDur" )
+ CONST_A(SettChatSessionTimeout , "ChatSessionTimeout" )
+ CONST_A(SettColumns , "Columns" )
+ CONST_A(SettFilterBBCodes , "FilterBBCodes" )
+ CONST_A(SettFilterRawRTF , "FilterRawRTF" )
+ CONST_A(SettFilterWords , "FilterWords" )
+ CONST_A(SettGraphicsMode , "GraphicsMode" )
+ CONST_A(SettHeaderTooltips , "HeaderTooltips" )
+ CONST_A(SettHeaderTooltipsIfCustom, "HeaderTooltipsIfCustom")
+ CONST_A(SettHideContactMenuProtos , "HideContactMenuProtos" )
+ CONST_A(SettIgnoreAfter , "IgnoreAfter" )
+ CONST_A(SettIgnoreBefore , "IgnoreBefore" )
+ CONST_A(SettIgnoreOld , "IgnoreOld" )
+ CONST_A(SettMenuItem , "MenuItem" )
+ CONST_A(SettMergeContacts , "MergeContacts" )
+ CONST_A(SettMergeContactsGroups , "MergeContactsGroups" )
+ CONST_A(SettMergeMode , "MergeMode" )
+ CONST_A(SettMetaContactsMode , "MetaContactsMode" )
+ CONST_A(SettNickname , "Nickname" )
+ CONST_A(SettOmitByRank , "OmitByRank" )
+ CONST_A(SettOmitByTime , "OmitByTime" )
+ CONST_A(SettOmitByTimeDays , "OmitByTimeDays" )
+ CONST_A(SettOmitByValue , "OmitByValue" )
+ CONST_A(SettOmitByValueData , "OmitByValueData" )
+ CONST_A(SettOmitByValueLimit , "OmitByValueLimit" )
+ CONST_A(SettOmitContacts , "OmitContacts" )
+ CONST_A(SettOmitNumOnTop , "OmitNumOnTop" )
+ CONST_A(SettOmittedInTotals , "OmittedInTotals" )
+ CONST_A(SettOmittedInExtraRow , "OmittedInExtraRow" )
+ CONST_A(SettOnStartup , "OnStartup" )
+ CONST_A(SettOutput , "Output" )
+ CONST_A(SettOutputExtraFolder , "OutputExtraFolder" )
+ CONST_A(SettOutputExtraToFolder , "OutputExtraToFolder" )
+ CONST_A(SettOutputVariables , "OutputVariables" )
+ CONST_A(SettOverwriteAlways , "OverwriteAlways" )
+ CONST_A(SettPathToBrowser , "PathToBrowser" )
+ CONST_A(SettPNGMode , "PNGMode" )
+ CONST_A(SettProtosIgnore , "ProtosIgnore2" )
+ CONST_A(SettRemoveEmptyContacts , "RemoveEmptyContacts" )
+ CONST_A(SettRemoveInChatsZero , "RemoveInChatsZero" )
+ CONST_A(SettRemoveInBytesZero , "RemoveInBytesZero" )
+ CONST_A(SettRemoveOutChatsZero , "RemoveOutChatsZero" )
+ CONST_A(SettRemoveOutBytesZero , "RemoveOutBytesZero" )
+ CONST_A(SettShowContactMenu , "ShowContactMenu" )
+ CONST_A(SettShowContactMenuPseudo , "ShowContactMenuPseudo" )
+ CONST_A(SettShowMenuSub , "ShowMenuSub" )
+ CONST_A(SettSort , "Sort" )
+ CONST_A(SettTableHeader , "TableHeader" )
+ CONST_A(SettTableHeaderRepeat , "TableHeaderRepeat" )
+ CONST_A(SettTableHeaderVerbose , "TableHeaderVerbose" )
+ CONST_A(SettThreadLowPriority , "ThreadLowPriority" )
+ CONST_A(SettWordDelimiters , "WordDelimiters" )
+ CONST_A(SettExclude , "Exclude" )
+
+ // clist module settings
+ CONST_A(ModCList , "CList" )
+ CONST_A(SettGroup , "Group" )
+
+ // column tags
+ CONST_T(ColChatDuration , "chatduration" )
+ CONST_T(ColEvents , "events" )
+ CONST_T(ColGroup , "group" )
+ CONST_T(ColInOut , "inouttext" )
+ CONST_T(ColInOutGraph , "inout" )
+ CONST_T(ColNick , "nick" )
+ CONST_T(ColProtocol , "protocol" )
+ CONST_T(ColRank , "rank" )
+ CONST_T(ColSplit , "split" )
+ CONST_T(ColSplitTimeline , "splittimeline" )
+ CONST_T(ColTimeline , "timeline" )
+ CONST_T(ColWordCount , "wordcount" )
+ CONST_T(ColWords , "commonwords" )
+
+ // suffix for column-specific settings
+ CONST_T(SuffixData , "/data" )
+
+ // suffix for shared column data (filter words)
+ CONST_T(SuffixWords , "/words" )
+
+ // keys for common column settings
+ CONST_T(KeyGUID , "guid" )
+ CONST_T(KeyEnabled , "enabled" )
+ CONST_T(KeyTitle , "title" )
+
+ // keys for column-specific settings
+ CONST_T(KeyAbsolute , "absolute" ) // InOut, InOutGraph
+ CONST_T(KeyAbsTime , "abs_time" ) // InOut, InOutGraph
+ CONST_T(KeyBlocks , "blocks" ) // Split, SplitTimeline
+ CONST_T(KeyBlockUnit , "block_unit" ) // Split, SplitTimeline
+ CONST_T(KeyContactCount , "contact_count" ) // Nick
+ CONST_T(KeyCustomGroup , "custom_group" ) // SplitTimeline
+ CONST_T(KeyDays , "days" ) // Timeline
+ CONST_T(KeyDetail , "detail" ) // ChatDuration, InOutGraph, Nick, Split, Timeline, WordCount, Words
+ CONST_T(KeyDetailInOut , "detail_inout" ) // Words
+ CONST_T(KeyDetailInvert , "detail_invert" ) // InOutGraph
+ CONST_T(KeyDetailPercent , "detail_percent" ) // InOutGraph
+ CONST_T(KeyDOWGroup , "dow_group" ) // SplitTimeline
+ CONST_T(KeyFilterLinks , "filter_links" ) // [Base]Words
+ CONST_T(KeyFilterWords , "filter_words" ) // [Base]Words
+ CONST_T(KeyGraph , "graph" ) // ChatDuration
+ CONST_T(KeyGraphAlign , "graph_align" ) // Split, SplitTimeline
+ CONST_T(KeyGraphPercent , "graph_percent" ) // InOutGraph
+ CONST_T(KeyHODGroup , "hod_group" ) // SplitTimeline
+ CONST_T(KeyIgnoreOld , "ignore_old" ) // Timeline
+ CONST_T(KeyInOutColor , "in_out_color" ) // Words
+ CONST_T(KeyMaxLength , "max_length" ) // [Base]Words
+ CONST_T(KeyMinLength , "min_length" ) // [Base]Words
+ CONST_T(KeyNum , "num" ) // Words, **keys for common column settings**
+ CONST_T(KeyOffset , "offset" ) // Words
+ CONST_T(KeyShowSum , "show_sum" ) // InOutGraph
+ CONST_T(KeySource , "source" ) // [Base]Words, Events, InOut, InOutGraph, Split, SplitTimeline, Timeline
+ CONST_T(KeySourceType , "source_type" ) // Split, SplitTimeline, Timeline
+ CONST_T(KeyTopPerColumn , "top_per_column" ) // SplitTimeline
+ CONST_T(KeyUnitsPerBlock , "units_per_block" ) // Split, SplitTimeline
+ CONST_T(KeyVisMode , "vis_mode" ) // ChatDuration, Split, SplitTimeline, WordCount, Words
+
+ // keeys for shared column data (filter words)
+ CONST_T(KeyID , "id" )
+ CONST_T(KeyName , "name" )
+ CONST_T(KeyMode , "mode" )
+ CONST_T(KeyNumWords , "num_words" )
+
+ // keys for sort settings
+ CONST_T(KeyAsc , "asc" )
+ CONST_T(KeyBy , "by" )
+
+ // miranda services created by historystats
+ CONST_A(SvcConfigure , "HistoryStats/Configure" )
+ CONST_A(SvcCreateStatistics , "HistoryStats/CreateFile" )
+ CONST_A(SvcShowStatistics , "HistoryStats/ShowFile" )
+ CONST_A(SvcToggleExclude , "HistoryStats/ToggleExclude")
+#if defined(HISTORYSTATS_HISTORYCOPY)
+ CONST_A(SvcHistoryCopy , "HistoryStats/HistoryCopy" )
+ CONST_A(SvcHistoryPaste , "HistoryStats/HistoryPaste" )
+#endif
+
+ // min/max time
+ const DWORD MinDateTime = 0x00000000;
+ const DWORD MaxDateTime = 0xFFFFFFFF;
+
+ // default colors for html output
+ const COLORREF ColorOut = RGB(0xE8, 0x64, 0x1B);
+ const COLORREF ColorIn = RGB(0x7C, 0xC6, 0x23);
+ const COLORREF ColorBar = RGB(0x05, 0x81, 0xB4);
+
+ const COLORREF ColorIOLine = RGB(0xBF, 0xBF, 0xBF);
+ const COLORREF ColorBarLine = RGB(0x02, 0x3F, 0x59);
+ const COLORREF ColorBarBack = RGB(0xBF, 0xBF, 0xBF);
+
+ const COLORREF ColorBack = RGB(0xFF, 0xFF, 0xFF);
+ const COLORREF ColorBorder = RGB(0x7F, 0x7F, 0x7F);
+ const COLORREF ColorHeader = RGB(0xDF, 0xDF, 0xDF);
+ const COLORREF ColorOmitted = RGB(0xDF, 0xDF, 0xDF);
+ const COLORREF ColorTotals = RGB(0xDF, 0xDF, 0xDF);
+}
+
+#undef CONST_T
+#undef CONST_A
+#undef CONST_W
+
+#endif // HISTORYSTATS_GUARD__CONSTS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/_format.h b/plugins/HistoryStats/src/_format.h
new file mode 100644
index 0000000000..9e53e21e8b
--- /dev/null
+++ b/plugins/HistoryStats/src/_format.h
@@ -0,0 +1,176 @@
+#if !defined(HISTORYSTATS_GUARD__FORMAT_H)
+#define HISTORYSTATS_GUARD__FORMAT_H
+
+/*
+ * ext::basic_format and helper routines
+ */
+
+namespace ext
+{
+ template<typename T_>
+ class basic_format
+ {
+ public:
+ typedef typename T_ char_type;
+ typedef typename std::basic_string<T_> str_type;
+ typedef typename std::basic_string<T_>::size_type size_type;
+ typedef typename ext::basic_strfunc<T_> _Func;
+
+ private:
+ str_type m_Str;
+ char_type m_Sep;
+ size_type m_NextPos;
+
+ public:
+ explicit basic_format(const str_type& str, char_type sep = muC('|'))
+ : m_Str(str), m_Sep(sep), m_NextPos(0)
+ {
+ }
+
+ const str_type& str() const
+ {
+ return m_Str;
+ }
+
+ basic_format& operator %(const char_type* szValue)
+ {
+ str_type::size_type pos = m_Str.find(m_Sep, m_NextPos);
+
+ if (pos != str_type::npos)
+ {
+ size_type len = _Func::len(szValue);
+
+ m_Str.replace(pos, 1, szValue, len);
+ m_NextPos = pos + len;
+ }
+
+ return *this;
+ }
+
+ basic_format& operator %(const str_type& strValue)
+ {
+ return (*this % strValue.c_str());
+ }
+
+ basic_format& operator %(int nValue)
+ {
+ std::basic_ostringstream<char_type> oss;
+
+ oss << nValue;
+
+ return (*this % oss.str().c_str());
+ }
+ };
+
+ template<typename T_>
+ inline const std::basic_string<T_>& str(const basic_format<T_>& format)
+ {
+ return format.str();
+ }
+};
+
+template<typename T_>
+inline std::basic_ostream<T_>& operator <<(std::basic_ostream<T_>& os, const ext::basic_format<T_>& format)
+{
+ os << format.str();
+
+ return os;
+}
+
+/*
+ * ext::basic_kformat and helper routines
+ */
+
+namespace ext
+{
+ template<typename T_>
+ class basic_kformat
+ {
+ public:
+ typedef typename T_ char_type;
+ typedef typename std::basic_string<T_> str_type;
+ typedef typename std::basic_string<T_>::size_type size_type;
+ typedef typename ext::basic_strfunc<T_> _Func;
+
+ private:
+ str_type m_Str;
+ str_type m_Mask;
+ str_type m_CurKey;
+
+ public:
+ explicit basic_kformat(const str_type& str)
+ : m_Str(str), m_Mask(str.length(), muC('_'))
+ {
+ }
+
+ const str_type& str() const
+ {
+ return m_Str;
+ }
+
+ basic_kformat& operator %(const char_type* szKey)
+ {
+ m_CurKey = szKey;
+
+ return *this;
+ }
+
+ basic_kformat& operator *(const char_type* szValue)
+ {
+ if (!m_CurKey.empty())
+ {
+ ext::string::size_type pos = 0;
+ ext::string::size_type key_len = m_CurKey.length();
+ ext::string::size_type value_len = _Func::len(szValue);
+
+ while ((pos = m_Str.find(m_CurKey, pos)) != ext::string::npos)
+ {
+ if (m_Mask.substr(pos, key_len).find(muC('X')) == ext::string::npos)
+ {
+ // replace since we didn't replace here before
+ m_Str.replace(pos, key_len, szValue, value_len);
+ m_Mask.replace(pos, key_len, value_len, muC('X'));
+ pos += value_len;
+ }
+ else
+ {
+ // skip since we already replaced in this area
+ pos += key_len;
+ }
+ }
+ }
+
+ return *this;
+ }
+
+ basic_kformat& operator *(const str_type& strValue)
+ {
+ return (*this * strValue.c_str());
+ }
+
+ basic_kformat& operator *(int nValue)
+ {
+ std::basic_ostringstream<char_type> oss;
+
+ oss << nValue;
+
+ return (*this * oss.str().c_str());
+ }
+ };
+
+ template<typename T_>
+ inline const std::basic_string<T_>& str(const basic_kformat<T_>& format)
+ {
+ return format.str();
+ }
+}
+
+template<typename T_>
+inline std::basic_ostream<T_>& operator <<(std::basic_ostream<T_>& os, const ext::basic_kformat<T_>& format)
+{
+ os << format.str();
+
+ return os;
+}
+
+#endif // HISTORYSTATS_GUARD__FORMAT_H
diff --git a/plugins/HistoryStats/src/_globals.cpp b/plugins/HistoryStats/src/_globals.cpp
new file mode 100644
index 0000000000..23220fc5e2
--- /dev/null
+++ b/plugins/HistoryStats/src/_globals.cpp
@@ -0,0 +1 @@
+#include "_globals.h"
diff --git a/plugins/HistoryStats/src/_globals.h b/plugins/HistoryStats/src/_globals.h
new file mode 100644
index 0000000000..a4368cf083
--- /dev/null
+++ b/plugins/HistoryStats/src/_globals.h
@@ -0,0 +1,92 @@
+#if !defined(HISTORYSTATS_GUARD__GLOABLS_H)
+#define HISTORYSTATS_GUARD__GLOABLS_H
+
+#pragma warning(disable: 4018) // FIXME: supress "signed/unsigned mismatch" warnings
+
+/*
+ * nicer interface for miranda
+ */
+
+#define _CRT_SECURE_NO_WARNINGS
+#define _CRT_NON_CONFORMING_SWPRINTFS
+
+#pragma warning(disable:4267)
+
+#include "mu_common.h"
+
+/*
+ * essential includes
+ */
+
+#include <commctrl.h>
+
+#include <cstdio>
+#include <cassert>
+#include <ctime>
+
+#include <string>
+#include <fstream>
+#include <sstream>
+
+/*
+ * some patterns and similar stuff we want to use everywhere
+ */
+
+#include "utils/pattern.h"
+
+/*
+ * some language 'extensions' (people will hate me because of this) and useful classes
+ */
+
+#include "_langext.h"
+#include "_strfunc.h"
+#include "_format.h"
+
+/*
+ * convenience typedefs
+ */
+
+namespace ext
+{
+ namespace w
+ {
+ const mu_wide* const endl = muW("\n");
+
+ typedef std::basic_string <mu_wide> string;
+ typedef std::basic_ofstream<mu_wide> ofstream;
+ typedef std::basic_ostream <mu_wide> ostream;
+ typedef ext::basic_strfunc <mu_wide> strfunc;
+ typedef ext::basic_format <mu_wide> format;
+ typedef ext::basic_kformat <mu_wide> kformat;
+ }
+
+ namespace a
+ {
+ const mu_ansi* const endl = muA("\n");
+
+ typedef std::basic_string <mu_ansi> string;
+ typedef std::basic_ofstream<mu_ansi> ofstream;
+ typedef std::basic_ostream <mu_ansi> ostream;
+ typedef ext::basic_strfunc <mu_ansi> strfunc;
+ typedef ext::basic_format <mu_ansi> format;
+ typedef ext::basic_kformat <mu_ansi> kformat;
+ }
+
+ // choose the right T-style namespace for this compilation
+ namespace t = MU_DO_BOTH(a, w);
+
+ // import T-style classes for easier access
+ using namespace t;
+
+ // helper functions
+ inline const mu_text* i18n(const mu_text* str) { return mu::langpack::translateString(str); }
+}
+
+/*
+ * translation stuff
+ */
+
+#define I18N(x) x
+using ext::i18n;
+
+#endif // HISTORYSTATS_GUARD__GLOABLS_H
diff --git a/plugins/HistoryStats/src/_langext.h b/plugins/HistoryStats/src/_langext.h
new file mode 100644
index 0000000000..ffe1626643
--- /dev/null
+++ b/plugins/HistoryStats/src/_langext.h
@@ -0,0 +1,35 @@
+#if !defined(HISTORYSTATS_GUARD__LANGEXT_H)
+#define HISTORYSTATS_GUARD__LANGEXT_H
+
+/*
+ * language "enhancements" - people will hate me for this
+ */
+
+#define bool_(c_bool) \
+ ((c_bool) ? true : false)
+
+#define BOOL_(cpp_bool) \
+ ((cpp_bool) ? TRUE : FALSE)
+
+#define array_len(array_var) \
+ (sizeof(array_var) / sizeof((array_var)[0]))
+
+#define range_each_(index_var, lo_bound, up_bound) \
+ for (int index_var = (lo_bound); index_var < (up_bound); ++index_var)
+
+#define upto_each_(index_var, up_bound) \
+ range_each_(index_var, 0, (up_bound))
+
+#define array_each_(index_var, array_var) \
+ range_each_(index_var, 0, array_len(array_var))
+
+#define vector_each_(index_var, vector_var) \
+ for (int index_var = 0; index_var != (vector_var).size(); ++index_var)
+
+#define iter_each_(cont_type, iter_var, cont_var) \
+ for (cont_type::iterator iter_var = (cont_var).begin(); iter_var != (cont_var).end(); ++iter_var)
+
+#define citer_each_(cont_type, iter_var, cont_var) \
+ for (cont_type::const_iterator iter_var = (cont_var).begin(); iter_var != (cont_var).end(); ++iter_var)
+
+#endif // HISTORYSTATS_GUARD__LANGEXT_H
diff --git a/plugins/HistoryStats/src/_strfunc.h b/plugins/HistoryStats/src/_strfunc.h
new file mode 100644
index 0000000000..6ff5d61237
--- /dev/null
+++ b/plugins/HistoryStats/src/_strfunc.h
@@ -0,0 +1,42 @@
+#if !defined(HISTORYSTATS_GUARD__STRFUNC_H)
+#define HISTORYSTATS_GUARD__STRFUNC_H
+
+namespace ext
+{
+ template<typename T_>
+ class basic_strfunc
+ {
+ };
+
+ template<>
+ class basic_strfunc<mu_ansi>
+ {
+ public:
+ static const mu_ansi* chr(const mu_ansi* string, mu_ansi c) { return strchr(string, c); }
+ static int cmp(const mu_ansi* string1, const mu_ansi* string2) { return strcmp(string1, string2); }
+ static int icmp(const mu_ansi* string1, const mu_ansi* string2) { return _stricmp(string1, string2); }
+ static int coll(const mu_ansi* string1, const mu_ansi* string2) { return strcoll(string1, string2); }
+ static int icoll(const mu_ansi* string1, const mu_ansi* string2) { return _stricoll(string1, string2); }
+ static const mu_ansi* str(const mu_ansi* string, const mu_ansi* strSearch) { return strstr(string, strSearch); }
+ static size_t len(const mu_ansi* string) { return strlen(string); }
+ static size_t ftime(mu_ansi* strDest, size_t maxsize, const mu_ansi* format, const struct tm* timeptr) { return strftime(strDest, maxsize, format, timeptr); }
+ static int sprintf(mu_ansi* buffer, const mu_ansi* format, ...) { va_list args; va_start(args, format); return vsprintf(buffer, format, args); }
+ };
+
+ template<>
+ class basic_strfunc<mu_wide>
+ {
+ public:
+ static const mu_wide* chr(const mu_wide* string, mu_wide c) { return wcschr(string, c); }
+ static int cmp(const mu_wide* string1, const mu_wide* string2) { return wcscmp(string1, string2); }
+ static int icmp(const mu_wide* string1, const mu_wide* string2) { return _wcsicmp(string1, string2); }
+ static int coll(const mu_wide* string1, const mu_wide* string2) { return wcscoll(string1, string2); }
+ static int icoll(const mu_wide* string1, const mu_wide* string2) { return _wcsicoll(string1, string2); }
+ static const mu_wide* str(const mu_wide* string, const mu_wide* strSearch) { return wcsstr(string, strSearch); }
+ static size_t len(const mu_wide* string) { return wcslen(string); }
+ static size_t ftime(mu_wide* strDest, size_t maxsize, const mu_wide* format, const struct tm* timeptr) { return wcsftime(strDest, maxsize, format, timeptr); }
+ static int sprintf(mu_wide* buffer, const mu_wide* format, ...) { va_list args; va_start(args, format); return vswprintf(buffer, format, args); }
+ };
+}
+
+#endif // HISTORYSTATS_GUARD__STRFUNC_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/bandctrl.cpp b/plugins/HistoryStats/src/bandctrl.cpp
new file mode 100644
index 0000000000..677ab9d90e
--- /dev/null
+++ b/plugins/HistoryStats/src/bandctrl.cpp
@@ -0,0 +1,73 @@
+#include "_globals.h"
+#include "bandctrl.h"
+
+/*
+ * BandCtrl
+ */
+
+void BandCtrl::setLayout(int nLayout)
+{
+ SendMessage(m_hBandWnd, BCM_SETLAYOUT, nLayout, 0);
+}
+
+HANDLE BandCtrl::addButton(DWORD dwFlags, HICON hIcon, DWORD dwData, const mu_text* szTooltip /* = NULL */, const mu_text* szText /* = NULL */)
+{
+ BCBUTTON bcb;
+
+ bcb.dwFlags = dwFlags | BCF_ICON | BCF_DATA | (szTooltip ? BCF_TOOLTIP : 0) | (szText ? BCF_TEXT : 0);
+ bcb.hIcon = hIcon;
+ bcb.dwData = dwData;
+ bcb.szTooltip = const_cast<mu_text*>(szTooltip);
+ bcb.szText = const_cast<mu_text*>(szText);
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hBandWnd, BCM_ADDBUTTON, 0, reinterpret_cast<LPARAM>(&bcb)));
+}
+
+bool BandCtrl::isButtonChecked(HANDLE hButton)
+{
+ return bool_(SendMessage(m_hBandWnd, BCM_ISBUTTONCHECKED, reinterpret_cast<WPARAM>(hButton), 0));
+}
+
+void BandCtrl::checkButton(HANDLE hButton, bool bCheck)
+{
+ SendMessage(m_hBandWnd, BCM_CHECKBUTTON, reinterpret_cast<WPARAM>(hButton), BOOL_(bCheck));
+}
+
+DWORD BandCtrl::getButtonData(HANDLE hButton)
+{
+ return SendMessage(m_hBandWnd, BCM_GETBUTTONDATA, reinterpret_cast<WPARAM>(hButton), 0);
+}
+
+void BandCtrl::setButtonData(HANDLE hButton, DWORD dwData)
+{
+ SendMessage(m_hBandWnd, BCM_SETBUTTONDATA, reinterpret_cast<WPARAM>(hButton), dwData);
+}
+
+bool BandCtrl::isButtonVisisble(HANDLE hButton)
+{
+ return bool_(SendMessage(m_hBandWnd, BCM_ISBUTTONVISIBLE, reinterpret_cast<WPARAM>(hButton), 0));
+}
+
+void BandCtrl::showButton(HANDLE hButton, bool bShow)
+{
+ SendMessage(m_hBandWnd, BCM_SHOWBUTTON, reinterpret_cast<WPARAM>(hButton), BOOL_(bShow));
+}
+
+RECT BandCtrl::getButtonRect(HANDLE hButton)
+{
+ RECT rButton;
+
+ SendMessage(m_hBandWnd, BCM_GETBUTTONRECT, reinterpret_cast<WPARAM>(hButton), reinterpret_cast<LPARAM>(&rButton));
+
+ return rButton;
+}
+
+bool BandCtrl::isButtonEnabled(HANDLE hButton)
+{
+ return bool_(SendMessage(m_hBandWnd, BCM_ISBUTTONENABLED, reinterpret_cast<WPARAM>(hButton), 0));
+}
+
+void BandCtrl::enableButton(HANDLE hButton, bool bEnable)
+{
+ SendMessage(m_hBandWnd, BCM_ENABLEBUTTON, reinterpret_cast<WPARAM>(hButton), BOOL_(bEnable));
+}
diff --git a/plugins/HistoryStats/src/bandctrl.h b/plugins/HistoryStats/src/bandctrl.h
new file mode 100644
index 0000000000..db3c5c81f7
--- /dev/null
+++ b/plugins/HistoryStats/src/bandctrl.h
@@ -0,0 +1,40 @@
+#if !defined(HISTORYSTATS_GUARD_BANDCTRL_H)
+#define HISTORYSTATS_GUARD_BANDCTRL_H
+
+#include "_globals.h"
+#include "bandctrldefs.h"
+
+/*
+ * BandCtrl
+ */
+
+class BandCtrl
+ : public BandCtrlDefs
+ , private pattern::NotCopyable<BandCtrl>
+{
+private:
+ HWND m_hBandWnd;
+
+public:
+ explicit BandCtrl(HWND hBandWnd = NULL) : m_hBandWnd(hBandWnd) { }
+ ~BandCtrl() { }
+
+public:
+ const BandCtrl& operator <<(HWND hBandWnd) { m_hBandWnd = hBandWnd; return *this; }
+ operator HWND() { return m_hBandWnd; }
+
+public:
+ void setLayout(int nLayout);
+ HANDLE addButton(DWORD dwFlags, HICON hIcon, DWORD dwData, const mu_text* szTooltip = NULL, const mu_text* szText = NULL);
+ bool isButtonChecked(HANDLE hButton);
+ void checkButton(HANDLE hButton, bool bCheck);
+ DWORD getButtonData(HANDLE hButton);
+ void setButtonData(HANDLE hButton, DWORD dwData);
+ bool isButtonVisisble(HANDLE hButton);
+ void showButton(HANDLE hButton, bool bShow);
+ RECT getButtonRect(HANDLE hButton);
+ bool isButtonEnabled(HANDLE hButton);
+ void enableButton(HANDLE hButton, bool bEnable);
+};
+
+#endif // HISTORYSTATS_GUARD_BANDCTRL_H
diff --git a/plugins/HistoryStats/src/bandctrldefs.h b/plugins/HistoryStats/src/bandctrldefs.h
new file mode 100644
index 0000000000..2109696a77
--- /dev/null
+++ b/plugins/HistoryStats/src/bandctrldefs.h
@@ -0,0 +1,59 @@
+#if !defined(HISTORYSTATS_GUARD_BANDCTRLDEFS_H)
+#define HISTORYSTATS_GUARD_BANDCTRLDEFS_H
+
+#include "_globals.h"
+
+/*
+ * BandCtrlDefs
+ */
+
+class BandCtrlDefs
+{
+public:
+ enum Message {
+ BCM_SETLAYOUT = WM_USER + 0, // (int nLayout, #) -> #
+ BCM_ADDBUTTON = WM_USER + 1, // (#, BCBUTTON* pButton) -> HANDLE hButton
+ BCM_ISBUTTONCHECKED = WM_USER + 2, // (HANDLE hButton, #) -> BOOL bChecked
+ BCM_CHECKBUTTON = WM_USER + 3, // (HANDLE hButton, BOOL bCheck) -> #
+ BCM_GETBUTTONDATA = WM_USER + 4, // (HANDLE hButton, #) -> DWORD dwData
+ BCM_SETBUTTONDATA = WM_USER + 5, // (HANDLE hButton, DWORD dwData) -> #
+ BCM_ISBUTTONVISIBLE = WM_USER + 6, // (HANDLE hButton, #) -> BOOL bVisible
+ BCM_SHOWBUTTON = WM_USER + 7, // (HANDLE hButton, BOOL bShow) -> #
+ BCM_GETBUTTONRECT = WM_USER + 8, // (HANDLE hButton, RECT* pRect) -> #
+ BCM_ISBUTTONENABLED = WM_USER + 9, // (HANDLE hButton, #) -> BOOL bEnabled
+ BCM_ENABLEBUTTON = WM_USER + 10, // (HANDLE hButton, BOOL bEnable) -> #
+ };
+
+ enum Notification {
+ BCN_CLICKED = NM_LAST - 1, // -> NMBANDCTRL
+ BCN_DROPDOWN = NM_LAST - 2, // -> NMBANDCTRL
+ };
+
+ enum ButtonFlags {
+ BCF_RIGHT = 0x001,
+ BCF_CHECKED = 0x002,
+ BCF_HIDDEN = 0x004,
+ BCF_TOOLTIP = 0x008,
+ BCF_TEXT = 0x010,
+ BCF_ICON = 0x020,
+ BCF_DATA = 0x040,
+ BCF_DROPDOWN = 0x080,
+ BCF_DISABLED = 0x100,
+ };
+
+ struct BCBUTTON {
+ DWORD dwFlags;
+ HICON hIcon;
+ mu_text* szText;
+ mu_text* szTooltip;
+ DWORD dwData;
+ };
+
+ struct NMBANDCTRL {
+ NMHDR hdr;
+ HANDLE hButton;
+ DWORD dwData;
+ };
+};
+
+#endif // HISTORYSTATS_GUARD_BANDCTRLDEFS_H
diff --git a/plugins/HistoryStats/src/bandctrlimpl.cpp b/plugins/HistoryStats/src/bandctrlimpl.cpp
new file mode 100644
index 0000000000..675077d7b4
--- /dev/null
+++ b/plugins/HistoryStats/src/bandctrlimpl.cpp
@@ -0,0 +1,1057 @@
+#include "_globals.h"
+#include "bandctrlimpl.h"
+
+#include "main.h"
+#include "resource.h"
+
+/*
+ * BandCtrlImpl
+ */
+
+const mu_text* BandCtrlImpl::m_ClassName = muT("HistoryStatsBand");
+const int BandCtrlImpl::m_PollId = 100;
+const int BandCtrlImpl::m_PollDelay = 50;
+
+LRESULT CALLBACK BandCtrlImpl::staticWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ BandCtrlImpl* pCtrl = reinterpret_cast<BandCtrlImpl*>(GetWindowLong(hWnd, 0));
+
+ switch (msg)
+ {
+ case WM_NCCREATE:
+ pCtrl = new BandCtrlImpl(hWnd, reinterpret_cast<int>(reinterpret_cast<CREATESTRUCT*>(lParam)->hMenu));
+ SetWindowLong(hWnd, 0, reinterpret_cast<LONG>(pCtrl));
+ return TRUE;
+
+ case WM_DESTROY:
+ delete pCtrl;
+ SetWindowLong(hWnd, 0, 0);
+ return 0;
+
+ case WM_GETDLGCODE:
+ return DLGC_WANTARROWS;
+
+ case WM_SETFOCUS:
+ pCtrl->onWMSetFocus();
+ return 0;
+
+ case WM_KILLFOCUS:
+ if (pCtrl->m_nCurFocused != -1)
+ {
+ pCtrl->m_nCurFocused = -1;
+ InvalidateRect(pCtrl->m_hWnd, NULL, TRUE);
+ }
+ return 0;
+
+ case WM_ENABLE:
+ InvalidateRect(pCtrl->m_hWnd, NULL, TRUE);
+ return 0;
+
+ case WM_GETFONT:
+ return reinterpret_cast<LRESULT>(pCtrl->m_hFont);
+
+ case WM_SETFONT:
+ pCtrl->m_hFont = reinterpret_cast<HFONT>(wParam);
+ return 0;
+
+ case WM_WINDOWPOSCHANGED:
+ pCtrl->recalcButtonRects();
+ InvalidateRect(pCtrl->m_hWnd, NULL, TRUE);
+ return 0;
+
+ case WM_KEYDOWN:
+ pCtrl->onWMKeyDown(wParam);
+ return 0;
+
+ case WM_KEYUP:
+ pCtrl->onWMKeyUp(wParam);
+ return 0;
+
+ case WM_MOUSEMOVE:
+ pCtrl->onWMMouseMove(MAKEPOINTS(lParam));
+ return 0;
+
+ case WM_MOUSELEAVE:
+ pCtrl->onWMMouseLeave();
+ return 0;
+
+ case WM_TIMER:
+ if (wParam == m_PollId)
+ {
+ RECT rect;
+ POINT pt;
+
+ GetWindowRect(pCtrl->m_hWnd, &rect);
+ GetCursorPos(&pt);
+
+ if (!PtInRect(&rect, pt))
+ {
+ PostMessage(pCtrl->m_hWnd, WM_MOUSELEAVE, 0, 0);
+ KillTimer(pCtrl->m_hWnd, m_PollId);
+ }
+ }
+ return 0;
+
+ case WM_LBUTTONDOWN:
+ pCtrl->onWMLButtonDown(MAKEPOINTS(lParam));
+ return 0;
+
+ case WM_LBUTTONUP:
+ pCtrl->onWMLButtonUp(MAKEPOINTS(lParam));
+ return 0;
+
+ case WM_PAINT:
+ pCtrl->onWMPaint();
+ return 0;
+
+ case WM_ERASEBKGND:
+ return TRUE;
+
+ case WM_THEMECHANGED:
+ pCtrl->reloadTheme();
+ return 0;
+
+ case BCM_ADDBUTTON:
+ return pCtrl->onBCMAddButton(reinterpret_cast<BCBUTTON*>(lParam));
+
+ case BCM_ISBUTTONCHECKED:
+ assert(wParam >= 1 && wParam <= pCtrl->m_Items.size());
+ return BOOL_(pCtrl->m_Items[wParam - 1].bChecked);
+
+ case BCM_CHECKBUTTON:
+ pCtrl->onBCMCheckButton(wParam - 1, bool_(lParam));
+ return 0;
+
+ case BCM_GETBUTTONDATA:
+ assert(wParam >= 1 && wParam <= pCtrl->m_Items.size());
+ return pCtrl->m_Items[wParam - 1].dwData;
+
+ case BCM_SETBUTTONDATA:
+ assert(wParam >= 1 && wParam <= pCtrl->m_Items.size());
+ pCtrl->m_Items[wParam - 1].dwData = static_cast<DWORD>(lParam);
+ return 0;
+
+ case BCM_ISBUTTONVISIBLE:
+ assert(wParam >= 1 && wParam <= pCtrl->m_Items.size());
+ return BOOL_(pCtrl->m_Items[wParam - 1].bVisible);
+
+ case BCM_SHOWBUTTON:
+ pCtrl->onBCMShowButton(wParam - 1, bool_(lParam));
+ return 0;
+
+ case BCM_SETLAYOUT:
+ assert(static_cast<int>(wParam) >= 0);
+ pCtrl->m_nLayout = wParam;
+ pCtrl->recalcButtonRects();
+ InvalidateRect(pCtrl->m_hWnd, NULL, TRUE);
+ return 0;
+
+ case BCM_GETBUTTONRECT:
+ pCtrl->onBCMGetButtonRect(wParam - 1, reinterpret_cast<RECT*>(lParam));
+ return 0;
+
+ case BCM_ISBUTTONENABLED:
+ assert(wParam >= 1 && wParam <= pCtrl->m_Items.size());
+ return BOOL_(pCtrl->m_Items[wParam - 1].bEnabled);
+
+ case BCM_ENABLEBUTTON:
+ pCtrl->onBCMEnableButton(wParam - 1, bool_(lParam));
+ return 0;
+ }
+
+ return DefWindowProc(hWnd, msg, wParam, lParam);
+}
+
+bool BandCtrlImpl::registerClass()
+{
+ const WNDCLASSEX wcx = {
+ sizeof(wcx), // cbSize
+ 0, // style
+ staticWndProc, // lpfnWndProc
+ 0, // cbClsExtra
+ sizeof(BandCtrlImpl*), // cbWndExtra
+ g_hInst, // hInstance
+ NULL, // hIcon
+ NULL, // hCursor
+ NULL, // hbrBackground
+ NULL, // lpszMenuName
+ m_ClassName, // lpszClassName
+ NULL // hIconSm
+ };
+
+ if (!RegisterClassEx(&wcx))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+void BandCtrlImpl::unregisterClass()
+{
+ UnregisterClass(m_ClassName, g_hInst);
+}
+
+BandCtrlImpl::BandCtrlImpl(HWND hWnd, int nOwnId)
+ : m_hWnd(hWnd), m_nOwnId(nOwnId), m_hFont(NULL),
+ m_hTheme(NULL), m_hImageList(NULL), m_hImageListD(NULL), m_hTooltip(NULL),
+ m_nCurHot(-1), m_nCurFocused(-1), m_nCurPressed(-1), m_bCurPressedDD(false),
+ m_nLayout(0), m_nDDWidth(12), m_hDDIcon(NULL)
+{
+ m_IconSize.cx = m_IconSize.cy;
+ m_hDDIcon = reinterpret_cast<HICON>(LoadImage(g_hInst, MAKEINTRESOURCE(IDI_DROPDOWN), IMAGE_ICON, OS::smIconCX(), OS::smIconCY(), 0));
+
+ reloadTheme();
+}
+
+BandCtrlImpl::~BandCtrlImpl()
+{
+ if (m_hTooltip)
+ {
+ DestroyWindow(m_hTooltip);
+ m_hTooltip = NULL;
+ }
+
+ if (m_hImageList)
+ {
+ ImageList_Destroy(m_hImageList);
+ m_hImageList = NULL;
+ }
+
+ if (m_hImageListD)
+ {
+ ImageList_Destroy(m_hImageListD);
+ m_hImageListD = NULL;
+ }
+
+ if (m_hTheme)
+ {
+ ThemeAPI::CloseThemeData(m_hTheme);
+ m_hTheme = NULL;
+ }
+
+ if (m_hDDIcon)
+ {
+ DestroyIcon(m_hDDIcon);
+ m_hDDIcon;
+ }
+}
+
+void BandCtrlImpl::onWMPaint()
+{
+ // start painting
+ PAINTSTRUCT ps;
+ HDC hRealDC;
+
+ if (!(hRealDC = BeginPaint(m_hWnd, &ps)))
+ {
+ return;
+ }
+
+ // get rect for painting
+ RECT rOut;
+
+ GetClientRect(m_hWnd, &rOut);
+
+ // setup memory DC for bufferd drawing
+ HDC hDC;
+ HBITMAP hMemBitmap, hOldBitmap;
+
+ hDC = CreateCompatibleDC(hRealDC);
+ hMemBitmap = CreateCompatibleBitmap(hRealDC, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top);
+ hOldBitmap = reinterpret_cast<HBITMAP>(SelectObject(hDC, hMemBitmap));
+ SetWindowOrgEx(hDC, ps.rcPaint.left, ps.rcPaint.top, NULL);
+
+ // fill background
+ bool bBandEnabled = bool_(IsWindowEnabled(m_hWnd));
+
+ SetBkColor(hDC, GetSysColor(bBandEnabled ? COLOR_WINDOW : COLOR_BTNFACE));
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rOut, NULL, 0, NULL);
+
+ // draw top and bottom line
+ if (bBandEnabled)
+ {
+ RECT rLine = { rOut.left, rOut.top, rOut.right, rOut.top + 1 };
+
+ SetBkColor(hDC, GetSysColor(COLOR_3DSHADOW));
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rLine, NULL, 0, NULL);
+
+ rLine.top = (rLine.bottom = rOut.bottom) - 1;
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rLine, NULL, 0, NULL);
+ }
+
+ // draw items
+ HGDIOBJ hOldFont = SelectObject(hDC, m_hFont);
+ SIZE textSize;
+
+ GetTextExtentPoint32(hDC, muT("X"), 1, &textSize);
+ SetBkMode(hDC, TRANSPARENT);
+ SetTextColor(hDC, GetSysColor(bBandEnabled ? COLOR_BTNTEXT : COLOR_GRAYTEXT));
+
+ vector_each_(i, m_Items)
+ {
+ if (m_Items[i].bVisible)
+ {
+ drawButton(hDC, i, textSize.cy, bBandEnabled);
+ }
+ }
+
+ SelectObject(hDC, hOldFont);
+
+ // write back memory DC
+ BitBlt(hRealDC, ps.rcPaint.left, ps.rcPaint.top, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top, hDC, ps.rcPaint.left, ps.rcPaint.top, SRCCOPY);
+ SelectObject(hDC, hOldBitmap);
+ DeleteObject(hOldBitmap);
+ DeleteDC(hDC);
+
+ // end painting
+ EndPaint(m_hWnd, &ps);
+}
+
+void BandCtrlImpl::drawButton(HDC hDC, int nItem, int textHeight, bool bBandEnabled)
+{
+ const ItemData& item = m_Items[nItem];
+
+ bool bFocused = (nItem == m_nCurFocused);
+ bool bHot = (nItem == m_nCurHot);
+ bool bPressed = (nItem == m_nCurPressed);
+ bool bEnabled = bBandEnabled && item.bEnabled;
+
+ // MEMO: beautified keyboard focus, remove following two lines to get back ugly one
+ bHot = bHot || bFocused;
+ bFocused = false;
+
+ RECT rItem = item.rItem;
+
+ if (item.bDropDown)
+ {
+ RECT rDropDown = rItem;
+
+ rDropDown.left = rDropDown.right - m_nDDWidth;
+ rItem.right -= m_nDDWidth;
+
+ if (m_hTheme)
+ {
+ int state = TS_DISABLED;
+
+ if (bEnabled)
+ {
+ state = bPressed ? (m_bCurPressedDD ? TS_PRESSED : TS_HOT) : (item.bChecked ? (bHot ? TS_HOTCHECKED : TS_CHECKED) : (bHot ? TS_HOT : TS_NORMAL));
+ }
+
+ ThemeAPI::DrawThemeBackground(m_hTheme, hDC, TP_SPLITBUTTONDROPDOWN, state, &rDropDown, NULL);
+ }
+ else
+ {
+ --rDropDown.left;
+
+ UINT state = 0;
+
+ if (bEnabled)
+ {
+ state = bPressed ? (m_bCurPressedDD ? DFCS_FLAT | DFCS_PUSHED : DFCS_FLAT) : (bHot ? DFCS_FLAT : (item.bChecked ? DFCS_FLAT | DFCS_CHECKED : 0));
+ }
+
+ if (state != 0)
+ {
+ DrawFrameControl(hDC, &rDropDown, DFC_BUTTON, DFCS_BUTTONPUSH | state);
+ }
+
+ int x = rDropDown.left + (rDropDown.right - rDropDown.left - OS::smIconCX()) / 2;
+ int y = rDropDown.top + (rDropDown.bottom - rDropDown.top - OS::smIconCY()) / 2;
+
+ DrawState(hDC, NULL, NULL, reinterpret_cast<LPARAM>(m_hDDIcon), 0, x, y, m_IconSize.cx, m_IconSize.cy, DST_ICON | (bEnabled ? 0 : DSS_DISABLED));
+ }
+ }
+
+ if (m_hTheme)
+ {
+ int state = TS_DISABLED;
+ int part = item.bDropDown ? TP_SPLITBUTTON : TP_BUTTON;
+
+ if (bEnabled)
+ {
+ state = bPressed ? (!m_bCurPressedDD ? TS_PRESSED : TS_HOT) : (item.bChecked ? (bHot ? TS_HOTCHECKED : TS_CHECKED) : (bHot ? TS_HOT : TS_NORMAL));
+ }
+
+ ThemeAPI::DrawThemeBackground(m_hTheme, hDC, part, state, &rItem, NULL);
+ }
+ else
+ {
+ UINT state = 0;
+
+ if (bEnabled)
+ {
+ state = bPressed ? (!m_bCurPressedDD ? DFCS_FLAT | DFCS_PUSHED : DFCS_FLAT) : (bHot ? DFCS_FLAT : (item.bChecked ? DFCS_FLAT | DFCS_CHECKED : 0));
+ }
+
+ if (state != 0)
+ {
+ DrawFrameControl(hDC, &rItem, DFC_BUTTON, DFCS_BUTTONPUSH | state);
+ }
+ }
+
+ InflateRect(&rItem, -3, -3);
+
+ if (!item.text.empty())
+ {
+ RECT rText = rItem;
+
+ rText.top += (rItem.bottom - rItem.top + m_IconSize.cy - textHeight) / 2;
+ rItem.bottom -= textHeight;
+
+ DrawText(
+ hDC,
+ item.text.c_str(),
+ item.text.length(),
+ &rText,
+ DT_TOP | DT_CENTER | DT_END_ELLIPSIS | DT_WORD_ELLIPSIS | DT_NOPREFIX | DT_SINGLELINE);
+ }
+
+ if (item.nIcon != -1)
+ {
+ int x = rItem.left + (rItem.right - rItem.left - m_IconSize.cx) / 2;
+ int y = rItem.top + (rItem.bottom - rItem.top - m_IconSize.cy) / 2;
+
+ if (bPressed && !m_bCurPressedDD)
+ {
+ ++x;
+ ++y;
+ }
+
+ if (bEnabled)
+ {
+ ImageList_Draw(
+ m_hImageList,
+ item.nIcon,
+ hDC,
+ x,
+ y,
+ ILD_NORMAL);
+ }
+ else if (item.nIconD != -1)
+ {
+ ImageList_Draw(
+ m_hImageListD,
+ item.nIconD,
+ hDC,
+ x,
+ y,
+ ILD_NORMAL);
+ }
+ else
+ {
+ HICON hIcon = ImageList_GetIcon(m_hImageList, item.nIcon, 0);
+
+ DrawState(hDC, NULL, NULL, reinterpret_cast<LPARAM>(hIcon), 0, x, y, m_IconSize.cx, m_IconSize.cy, DST_ICON | DSS_DISABLED);
+ DestroyIcon(hIcon);
+ }
+ }
+
+ if (bFocused)
+ {
+ rItem = item.rItem;
+
+ InflateRect(&rItem, -2, -2);
+ DrawFocusRect(hDC, &rItem);
+ }
+}
+
+void BandCtrlImpl::reloadTheme()
+{
+ if (m_hTheme)
+ {
+ ThemeAPI::CloseThemeData(m_hTheme);
+ m_hTheme = NULL;
+ }
+
+ m_nDDWidth = 12;
+
+ if (ThemeAPI::useTheme())
+ {
+ m_hTheme = ThemeAPI::OpenThemeData(0, muW("TOOLBAR"));
+
+ /*
+ SIZE sizeMin;
+ HDC hDC = GetDC(NULL);
+
+ ThemeAPI::GetThemePartSize(m_hTheme, hDC, TP_SPLITBUTTONDROPDOWN, TS_NORMAL, NULL, TS_DRAW, &sizeMin);
+ ReleaseDC(NULL, hDC);
+
+ m_nDDWidth = sizeMin.cx;
+ */
+ }
+
+ recalcButtonRects();
+}
+
+HICON BandCtrlImpl::convertToGray(HICON hIcon)
+{
+ // quick and dirty conversion to grayscale
+ // preserves transparency
+ // works only for 32bit icons
+
+ HICON hIconDisabled = NULL;
+ ICONINFO ii;
+
+ if (!GetIconInfo(hIcon, &ii))
+ {
+ return NULL;
+ }
+
+ BITMAP bmp;
+
+ if (GetObject(ii.hbmColor, sizeof(bmp), &bmp) && bmp.bmBitsPixel == 32)
+ {
+ int nSize = bmp.bmHeight * bmp.bmWidthBytes;
+ BYTE* pBits = new BYTE[nSize];
+
+ if (GetBitmapBits(ii.hbmColor, nSize, pBits))
+ {
+ for (int y = 0; y < bmp.bmHeight; ++y)
+ {
+ BYTE* pLine = pBits + y * bmp.bmWidthBytes;
+
+ for (int x = 0; x < bmp.bmWidth; ++x)
+ {
+ DWORD color = reinterpret_cast<DWORD*>(pLine)[x];
+ BYTE gray = (77 * GetBValue(color) + 150 * GetGValue(color) + 28 * GetRValue(color)) / 255;
+
+ color = (color & 0xFF000000) | RGB(gray, gray, gray);
+
+ reinterpret_cast<DWORD*>(pLine)[x] = color;
+ }
+ }
+
+ SetBitmapBits(ii.hbmColor, nSize, pBits);
+
+ hIconDisabled = CreateIconIndirect(&ii);
+ }
+
+ delete pBits;
+ }
+
+ DeleteObject(ii.hbmColor);
+ DeleteObject(ii.hbmMask);
+
+ return hIconDisabled;
+}
+
+int BandCtrlImpl::onBCMAddButton(BCBUTTON* pButton)
+{
+ assert(pButton);
+
+ m_Items.push_back(ItemData());
+
+ ItemData& id = m_Items.back();
+
+ id.bRight = bool_(pButton->dwFlags & BCF_RIGHT);
+ id.bChecked = bool_(pButton->dwFlags & BCF_CHECKED);
+ id.bVisible = !(pButton->dwFlags & BCF_HIDDEN);
+ id.bDropDown = bool_(pButton->dwFlags & BCF_DROPDOWN);
+ id.text = (pButton->dwFlags & BCF_TEXT) ? pButton->szText : muT("");
+ id.tooltip = (pButton->dwFlags & BCF_TOOLTIP) ? pButton->szTooltip : muT("");
+ id.uTTId = -1;
+ id.dwData = (pButton->dwFlags & BCF_DATA) ? pButton->dwData : 0;
+ id.bEnabled = !(pButton->dwFlags & BCF_DISABLED);
+ id.nIcon = -1;
+ id.nIconD = -1;
+
+ if (pButton->dwFlags & BCF_ICON)
+ {
+ // create an image list, if needed
+ if (!m_hImageList)
+ {
+ // guess image size from first inserted icon
+ ICONINFO ii;
+
+ if (GetIconInfo(pButton->hIcon, &ii))
+ {
+ BITMAP bmp;
+
+ if (GetObject(ii.hbmColor, sizeof(bmp), &bmp))
+ {
+ m_IconSize.cx = bmp.bmWidth;
+ m_IconSize.cy = bmp.bmHeight;
+ }
+
+ DeleteObject(ii.hbmColor);
+ DeleteObject(ii.hbmMask);
+ }
+
+ m_hImageList = ImageList_Create(m_IconSize.cx, m_IconSize.cy, OS::imageListColor() | ILC_MASK, 5, 5);
+ }
+
+ // insert icon into image list
+ id.nIcon = ImageList_AddIcon(m_hImageList, pButton->hIcon);
+
+ // insert disabled icon into image list
+ HICON hIconDisabled = convertToGray(pButton->hIcon);
+
+ if (hIconDisabled)
+ {
+ if (!m_hImageListD)
+ {
+ m_hImageListD = ImageList_Create(m_IconSize.cx, m_IconSize.cy, OS::imageListColor() | ILC_MASK, 5, 5);
+ }
+
+ id.nIconD = ImageList_AddIcon(m_hImageListD, hIconDisabled);
+
+ DestroyIcon(hIconDisabled);
+ }
+ }
+
+ // atomatically adds tooltip, if needed
+ recalcButtonRects();
+
+ if (id.bVisible)
+ {
+ InvalidateRect(m_hWnd, &id.rItem, TRUE);
+ }
+
+ return m_Items.size();
+}
+
+void BandCtrlImpl::onBCMCheckButton(int nItem, bool bCheck)
+{
+ assert(nItem >= 0 && nItem < m_Items.size());
+
+ ItemData& id = m_Items[nItem];
+
+ if (bCheck != id.bChecked)
+ {
+ id.bChecked = bCheck;
+ InvalidateRect(m_hWnd, &id.rItem, TRUE);
+ }
+}
+
+void BandCtrlImpl::onBCMShowButton(int nItem, bool bShow)
+{
+ assert(nItem >= 0 && nItem < m_Items.size());
+
+ ItemData& id = m_Items[nItem];
+
+ if (bShow != id.bVisible)
+ {
+ id.bVisible = bShow;
+ recalcButtonRects();
+ InvalidateRect(m_hWnd, NULL, TRUE);
+ }
+}
+
+void BandCtrlImpl::onBCMGetButtonRect(int nItem, RECT* pRect)
+{
+ assert(nItem >= 0 && nItem < m_Items.size());
+ assert(pRect);
+
+ *pRect = m_Items[nItem].rItem;
+}
+
+void BandCtrlImpl::onBCMEnableButton(int nItem, bool bEnable)
+{
+ assert(nItem >= 0 && nItem < m_Items.size());
+
+ ItemData& id = m_Items[nItem];
+
+ if (bEnable != id.bEnabled)
+ {
+ id.bEnabled = bEnable;
+ InvalidateRect(m_hWnd, NULL, TRUE);
+ }
+}
+
+void BandCtrlImpl::recalcButtonRects()
+{
+ RECT rOut;
+
+ GetClientRect(m_hWnd, &rOut);
+ InflateRect(&rOut, -2, -3);
+
+ int itemHeight = rOut.bottom - rOut.top;
+ int itemWidth = itemHeight;
+
+ if (m_nLayout > 0)
+ {
+ itemWidth = (rOut.right - rOut.left) / m_nLayout;
+ }
+
+ RECT rItemL = { rOut.left, rOut.top, rOut.left + itemWidth, rOut.top + itemHeight };
+ RECT rItemR = { rOut.right - itemWidth, rOut.top, rOut.right, rOut.top + itemHeight };
+
+ vector_each_(i, m_Items)
+ {
+ if (m_Items[i].bVisible)
+ {
+ // visible: give it a rect and advance
+ int nDDWidth = (m_Items[i].bDropDown && m_nLayout == 0) ? m_nDDWidth : 0;
+
+ if (m_Items[i].bRight)
+ {
+ m_Items[i].rItem = rItemR;
+ m_Items[i].rItem.left -= nDDWidth;
+ OffsetRect(&rItemR, -(itemWidth + nDDWidth), 0);
+ }
+ else
+ {
+ m_Items[i].rItem = rItemL;
+ m_Items[i].rItem.right += nDDWidth;
+ OffsetRect(&rItemL, itemWidth + nDDWidth, 0);
+ }
+ }
+
+ if (m_Items[i].uTTId != -1 && m_Items[i].bVisible)
+ {
+ // update tooltip rect, if we have a tooltip and are still visible
+ TOOLINFO ti = {
+ sizeof(TOOLINFO), // cbSize
+ TTF_SUBCLASS, // uFlags
+ m_hWnd, // hwnd
+ m_Items[i].uTTId, // uId
+ m_Items[i].rItem, // rect
+ NULL, // hInstance
+ const_cast<mu_text*>(m_Items[i].tooltip.c_str()), // lpszText
+ };
+
+ SendMessage(m_hTooltip, TTM_SETTOOLINFO, 0, reinterpret_cast<LPARAM>(&ti));
+ }
+ else if (m_Items[i].uTTId != -1 && !m_Items[i].bVisible)
+ {
+ // remove tooltip, if we have a tooltip but are no longer visible
+ TOOLINFO ti;
+
+ ti.cbSize = sizeof(TOOLINFO);
+ ti.hwnd = m_hWnd;
+ ti.uId = m_Items[i].uTTId;
+
+ SendMessage(m_hTooltip , TTM_DELTOOL, 0, reinterpret_cast<LPARAM>(&ti));
+
+ m_Items[i].uTTId = -1;
+ }
+ else if (m_Items[i].uTTId == -1 && m_Items[i].bVisible && !m_Items[i].tooltip.empty())
+ {
+ // add a tooltip, if we don't have a tooltip but are now visible
+ if (!m_hTooltip)
+ {
+ m_hTooltip = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, muT(""), WS_POPUP, 0, 0, 0, 0, NULL, NULL, g_hInst, NULL);
+ }
+
+ TOOLINFO ti = {
+ sizeof(TOOLINFO), // cbSize
+ TTF_SUBCLASS, // uFlags
+ m_hWnd, // hwnd
+ i + 1, // uId
+ m_Items[i].rItem, // rect
+ NULL, // hInstance
+ const_cast<mu_text*>(m_Items[i].tooltip.c_str()), // lpszText
+ };
+
+ if (SendMessage(m_hTooltip, TTM_ADDTOOL, 0, reinterpret_cast<LPARAM>(&ti)))
+ {
+ m_Items[i].uTTId = ti.uId;
+ }
+ }
+ }
+}
+
+int BandCtrlImpl::getNextButton(int nItem)
+{
+ if (nItem < 0 || nItem >= m_Items.size())
+ {
+ nItem = -1;
+ }
+
+ int nNext = nItem;
+ int nLastLeft = -1;
+ bool bLeft = !(nItem != -1 && m_Items[nItem].bRight);
+
+ vector_each_(i, m_Items)
+ {
+ if (m_Items[i].bVisible && !m_Items[i].bRight)
+ {
+ nLastLeft = i;
+ }
+ }
+
+ vector_each_(i, m_Items)
+ {
+ if (!m_Items[i].bVisible)
+ continue;
+
+ if (nItem == nLastLeft)
+ {
+ if (m_Items[i].bRight)
+ {
+ nNext = i;
+ }
+ }
+ else if (!bLeft)
+ {
+ if (m_Items[i].bRight && i < nItem)
+ {
+ nNext = i;
+ break;
+ }
+ }
+ else
+ {
+ if (!m_Items[i].bRight && i > nNext)
+ {
+ nNext = i;
+ break;
+ }
+ }
+ }
+
+ return nNext;
+}
+
+int BandCtrlImpl::getPrevButton(int nItem)
+{
+ if (nItem < 0 || nItem >= m_Items.size())
+ {
+ nItem = -1;
+ }
+
+ int nPrev = nItem;
+ int nFirstRight = -1;
+ bool bRight = (nItem != -1 && m_Items[nItem].bRight);
+
+ vector_each_(i, m_Items)
+ {
+ if (m_Items[i].bVisible && m_Items[i].bRight)
+ {
+ nFirstRight = i;
+ }
+ }
+
+ vector_each_(i, m_Items)
+ {
+ if (!m_Items[i].bVisible)
+ continue;
+
+ if (!bRight)
+ {
+ if (!m_Items[i].bRight && i < nItem)
+ {
+ nPrev = i;
+ }
+ }
+ else if (nItem == nFirstRight)
+ {
+ if (!m_Items[i].bRight)
+ {
+ nPrev = i;
+ }
+ }
+ else
+ {
+ if (m_Items[i].bRight && i > nPrev)
+ {
+ nPrev = i;
+ break;
+ }
+ }
+ }
+
+ return nPrev;
+}
+
+void BandCtrlImpl::fireEvent(UINT code, int nItem)
+{
+ NMBANDCTRL nmbc;
+
+ nmbc.hdr.code = code;
+ nmbc.hdr.hwndFrom = m_hWnd;
+ nmbc.hdr.idFrom = m_nOwnId;
+ nmbc.hButton = reinterpret_cast<HANDLE>(nItem + 1);
+ nmbc.dwData = m_Items[nItem].dwData;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmbc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmbc));
+}
+
+void BandCtrlImpl::onWMSetFocus()
+{
+ /*
+ int nFirst = getNextItem(-1);
+
+ if (nFirst != -1)
+ {
+ m_nCurFocused = nFirst;
+ InvalidateRect(m_hWnd, NULL, TRUE);
+ }
+ */
+
+ m_nCurFocused = -1;
+}
+
+
+void BandCtrlImpl::onWMKeyDown(int nVirtKey)
+{
+ if (GetKeyState(VK_CONTROL) & ~1 || GetKeyState(VK_SHIFT) & ~1)
+ {
+ return;
+ }
+
+ if (nVirtKey == VK_RIGHT)
+ {
+ int nNext = getNextButton(m_nCurFocused);
+
+ if (nNext != -1 && nNext != m_nCurFocused)
+ {
+ m_nCurFocused = nNext;
+ InvalidateRect(m_hWnd, NULL, TRUE);
+ }
+ }
+ else if (nVirtKey == VK_LEFT)
+ {
+ int nPrev = getPrevButton(m_nCurFocused);
+
+ if (nPrev != -1 && nPrev != m_nCurFocused)
+ {
+ m_nCurFocused = nPrev;
+ InvalidateRect(m_hWnd, NULL, TRUE);
+ }
+ }
+ else if (nVirtKey == VK_SPACE)
+ {
+ if (m_nCurFocused != -1 && m_nCurFocused < m_Items.size() && m_Items[m_nCurFocused].bEnabled)
+ {
+ m_nCurPressed = m_nCurFocused;
+ m_bCurPressedDD = false;
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+ }
+ }
+ else if (nVirtKey == VK_DOWN)
+ {
+ if (m_nCurFocused != -1 && m_nCurFocused < m_Items.size() && m_Items[m_nCurFocused].bDropDown && m_Items[m_nCurFocused].bEnabled)
+ {
+ m_nCurPressed = m_nCurFocused;
+ m_bCurPressedDD = true;
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+
+ fireEvent(BCN_DROPDOWN, m_nCurPressed);
+
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+ m_nCurPressed = -1;
+ m_bCurPressedDD = false;
+ }
+ }
+}
+
+void BandCtrlImpl::onWMKeyUp(int nVirtKey)
+{
+ if (GetKeyState(VK_CONTROL) & ~1 || GetKeyState(VK_SHIFT) & ~1)
+ {
+ return;
+ }
+
+ if (nVirtKey == VK_SPACE && m_nCurPressed != -1 && m_nCurPressed < m_Items.size())
+ {
+ if (m_nCurFocused == m_nCurPressed)
+ {
+ fireEvent(BCN_CLICKED, m_nCurPressed);
+ }
+
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+ m_nCurPressed = -1;
+ m_bCurPressedDD = false;
+ }
+}
+
+void BandCtrlImpl::onWMMouseLeave()
+{
+ int nOldHot = m_nCurHot;
+
+ m_nCurHot = -1;
+
+ if (nOldHot != -1 && nOldHot < m_Items.size())
+ {
+ InvalidateRect(m_hWnd, &m_Items[nOldHot].rItem, TRUE);
+ }
+}
+
+void BandCtrlImpl::onWMMouseMove(POINTS pts)
+{
+ POINT pt = { pts.x, pts.y };
+
+ if (m_nCurHot != -1 && m_nCurHot < m_Items.size())
+ {
+ if (!PtInRect(&m_Items[m_nCurHot].rItem, pt))
+ {
+ InvalidateRect(m_hWnd, &m_Items[m_nCurHot].rItem, TRUE);
+ m_nCurHot = -1;
+ }
+ }
+
+ if (m_nCurHot == -1)
+ {
+ vector_each_(i, m_Items)
+ {
+ if (PtInRect(&m_Items[i].rItem, pt) && m_Items[i].bVisible)
+ {
+ m_nCurHot = i;
+ InvalidateRect(m_hWnd, &m_Items[i].rItem, TRUE);
+ break;
+ }
+ }
+ }
+
+ if (m_nCurHot != -1)
+ {
+ SetTimer(m_hWnd, m_PollId, m_PollDelay, NULL);
+ }
+}
+
+void BandCtrlImpl::onWMLButtonDown(POINTS pts)
+{
+ POINT pt = { pts.x, pts.y };
+
+ if (m_nCurHot != -1 && m_nCurHot < m_Items.size() && m_Items[m_nCurHot].bEnabled)
+ {
+ if (PtInRect(&m_Items[m_nCurHot].rItem, pt))
+ {
+ m_nCurPressed = m_nCurHot;
+ m_bCurPressedDD = false;
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+ SetCapture(m_hWnd);
+
+ if (m_Items[m_nCurHot].bDropDown)
+ {
+ RECT rDropDown = m_Items[m_nCurHot].rItem;
+
+ rDropDown.left = rDropDown.right - m_nDDWidth;
+
+ if (PtInRect(&rDropDown, pt))
+ {
+ ReleaseCapture();
+ m_bCurPressedDD = true;
+
+ fireEvent(BCN_DROPDOWN, m_nCurPressed);
+
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+ m_nCurPressed = -1;
+ m_bCurPressedDD = false;
+ }
+ }
+ }
+ }
+}
+
+void BandCtrlImpl::onWMLButtonUp(POINTS pts)
+{
+ POINT pt = { pts.x, pts.y };
+
+ if (m_nCurPressed != -1 && m_nCurPressed < m_Items.size())
+ {
+ ReleaseCapture();
+
+ if (PtInRect(&m_Items[m_nCurPressed].rItem, pt))
+ {
+ fireEvent(BCN_CLICKED, m_nCurPressed);
+ }
+
+ InvalidateRect(m_hWnd, &m_Items[m_nCurPressed].rItem, TRUE);
+ m_nCurPressed = -1;
+ m_bCurPressedDD = false;
+ }
+}
diff --git a/plugins/HistoryStats/src/bandctrlimpl.h b/plugins/HistoryStats/src/bandctrlimpl.h
new file mode 100644
index 0000000000..7aa9ce8abb
--- /dev/null
+++ b/plugins/HistoryStats/src/bandctrlimpl.h
@@ -0,0 +1,92 @@
+#if !defined(HISTORYSTATS_GUARD_BANDCTRLIMPL_H)
+#define HISTORYSTATS_GUARD_BANDCTRLIMPL_H
+
+#include "_globals.h"
+#include "bandctrldefs.h"
+
+#include <vector>
+
+#include "themeapi.h"
+
+/*
+ * BandCtrlImpl
+ */
+
+class BandCtrlImpl
+ : public BandCtrlDefs
+ , private pattern::NotCopyable<BandCtrlImpl>
+{
+private:
+ struct ItemData {
+ bool bRight;
+ int nIcon;
+ int nIconD;
+ ext::string text;
+ ext::string tooltip;
+ bool bChecked;
+ bool bVisible;
+ bool bDropDown;
+ DWORD dwData;
+ RECT rItem;
+ UINT uTTId;
+ bool bEnabled;
+ };
+
+private:
+ static const mu_text* m_ClassName;
+ static const int m_PollId;
+ static const int m_PollDelay;
+
+private:
+ static LRESULT CALLBACK staticWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
+
+public:
+ static bool registerClass();
+ static void unregisterClass();
+
+private:
+ HWND m_hWnd;
+ int m_nOwnId;
+ HFONT m_hFont;
+ HTHEME m_hTheme;
+ std::vector<ItemData> m_Items;
+ HIMAGELIST m_hImageList;
+ HIMAGELIST m_hImageListD;
+ SIZE m_IconSize;
+ HWND m_hTooltip;
+ int m_nCurHot;
+ int m_nCurFocused;
+ int m_nCurPressed;
+ bool m_bCurPressedDD;
+ int m_nLayout;
+ int m_nDDWidth;
+ HICON m_hDDIcon;
+
+private:
+ explicit BandCtrlImpl(HWND hWnd, int nOwnId);
+ ~BandCtrlImpl();
+
+private:
+ void onWMPaint();
+ void drawButton(HDC hDC, int nItem, int textHeight, bool bBandEnabled);
+ void reloadTheme();
+ HICON convertToGray(HICON hIcon);
+ int onBCMAddButton(BCBUTTON* pButton);
+ void onBCMCheckButton(int nItem, bool bCheck);
+ void onBCMShowButton(int nItem, bool bShow);
+ void onBCMGetButtonRect(int nItem, RECT* pRect);
+ void onBCMEnableButton(int nItem, bool bEnable);
+ void recalcButtonRects();
+ int getNextButton(int nItem);
+ int getPrevButton(int nItem);
+ void fireEvent(UINT code, int nItem);
+ void onWMSetFocus();
+ void onWMKeyDown(int nVirtKey);
+ void onWMKeyUp(int nVirtKey);
+ void onWMMouseLeave();
+ void onWMMouseMove(POINTS pts);
+ void onWMLButtonDown(POINTS pts);
+ void onWMLButtonUp(POINTS pts);
+};
+
+#endif // HISTORYSTATS_GUARD_BANDCTRLIMPL_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/canvas.cpp b/plugins/HistoryStats/src/canvas.cpp
new file mode 100644
index 0000000000..54f87dec45
--- /dev/null
+++ b/plugins/HistoryStats/src/canvas.cpp
@@ -0,0 +1,212 @@
+#include "_globals.h"
+#include "canvas.h"
+
+void Canvas::updateTrans(BYTE* pData)
+{
+ // MEMO: this follwing transparency code only makes sense for m_nChannels == 4
+ assert(m_nChannels == 4);
+
+ // apply transparency, if any
+ if (m_bTransColor)
+ {
+ for (int y = 0; y < m_nHeight; ++y)
+ {
+ COLORREF* pValue = reinterpret_cast<COLORREF*>(pData + y * m_nLineLength);
+
+ for (int x = 0; x < m_nWidth; ++x)
+ {
+ *pValue = (*pValue & 0x00FFFFFF) | ((*pValue & 0x00FFFFFF) == m_TransColor ? 0x00000000 : 0xFF000000);
+ ++pValue;
+ }
+ }
+ }
+ else
+ {
+ for (int y = 0; y < m_nHeight; ++y)
+ {
+ COLORREF* pValue = reinterpret_cast<COLORREF*>(pData + y * m_nLineLength);
+
+ for (int x = 0; x < m_nWidth; ++x)
+ {
+ *pValue |= 0xFF000000;
+ ++pValue;
+ }
+ }
+ }
+}
+
+Canvas::Canvas(int nWidth, int nHeight)
+ : m_nChannels(4)
+ , m_nWidth(nWidth)
+ , m_nHeight(nHeight)
+ , m_nLineLength((m_nChannels * m_nWidth + 3) & ~0x3)
+ , m_bTransColor(false)
+ , m_TransColor(0)
+ , m_pBMIH(NULL)
+{
+}
+
+Canvas::~Canvas()
+{
+ if (m_hOldBmp)
+ {
+ SelectObject(m_hDC, m_hOldBmp);
+ }
+
+ if (m_hBmp)
+ {
+ DeleteObject(m_hBmp);
+ }
+
+ if (m_hDC)
+ {
+ DeleteDC(m_hDC);
+ }
+
+ delete m_pBMIH;
+}
+
+void Canvas::setTrans(COLORREF transColor, bool bFill /* = false */)
+{
+ m_bTransColor = true;
+ m_TransColor = transColor;
+
+ if (bFill)
+ {
+ fillBackground(transColor);
+ }
+}
+
+void Canvas::fillBackground(COLORREF bkColor)
+{
+ HDC hDC = beginDraw();
+
+ RECT rAll = { 0, 0, m_nWidth, m_nHeight };
+
+ SetBkColor(hDC, bkColor);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rAll, NULL, 0, NULL);
+
+ endDraw();
+}
+
+HDC Canvas::beginDraw()
+{
+ if (!m_pBMIH)
+ {
+ m_pBMIH = new BITMAPINFOHEADER;
+
+ m_pBMIH->biSize = sizeof(BITMAPINFOHEADER);
+ m_pBMIH->biWidth = m_nWidth;
+ m_pBMIH->biHeight = m_nHeight;
+ m_pBMIH->biPlanes = 1;
+ m_pBMIH->biBitCount = 8 * m_nChannels;
+ m_pBMIH->biCompression = BI_RGB;
+ m_pBMIH->biSizeImage = m_nChannels * m_nWidth * m_nHeight;
+ m_pBMIH->biXPelsPerMeter = 0;
+ m_pBMIH->biYPelsPerMeter = 0;
+ m_pBMIH->biClrUsed = 0;
+ m_pBMIH->biClrImportant = 0;
+
+ BYTE* pData = 0;
+
+ m_hDC = CreateCompatibleDC(NULL);
+ m_hBmp = CreateDIBSection(m_hDC, reinterpret_cast<BITMAPINFO*>(m_pBMIH), DIB_RGB_COLORS, reinterpret_cast<void**>(&pData), NULL, 0);
+ }
+
+ m_hOldBmp = SelectObject(m_hDC, m_hBmp);
+
+ return m_hDC;
+}
+
+void Canvas::endDraw()
+{
+ SelectObject(m_hDC, m_hOldBmp);
+ m_hOldBmp = NULL;
+}
+
+bool Canvas::getDigest(Digest& digest)
+{
+ // we don't have a digest if the image is uninitialized
+ if (!m_pBMIH)
+ return false;
+
+ // read data from DIB
+ int nSize = m_nLineLength * m_nHeight;
+ BYTE* pData = new BYTE[nSize];
+
+ ZeroMemory(pData, nSize);
+
+ if (GetDIBits(m_hDC, m_hBmp, 0, m_nHeight, pData, reinterpret_cast<BITMAPINFO*>(m_pBMIH), DIB_RGB_COLORS) != m_nHeight)
+ {
+ delete[] pData;
+ return false;
+ }
+
+ // apply transparency, if any
+ updateTrans(pData);
+
+ // calculate hash
+ mir_sha1_hash(pData, nSize, digest.m_Digest);
+
+ delete[] pData;
+ return true;
+}
+
+bool Canvas::writePNG(const mu_text* szFileName)
+{
+ // read data from DIB
+ BYTE* pData = new BYTE[m_nLineLength * m_nHeight];
+
+ if (GetDIBits(m_hDC, m_hBmp, 0, m_nHeight, pData, reinterpret_cast<BITMAPINFO*>(m_pBMIH), DIB_RGB_COLORS) != m_nHeight)
+ {
+ delete[] pData;
+
+ return false;
+ }
+
+ // apply transparency, if any
+ updateTrans(pData);
+
+ // calculate resulting image size
+ long png_len = 0;
+
+ if (!mu::png::dibToPng(m_pBMIH, pData, 0, &png_len) || png_len == 0)
+ {
+ delete[] pData;
+
+ return false;
+ }
+
+ // get the resulting image data
+ BYTE* pRawPNG = new BYTE[png_len];
+
+ png_len = 0;
+
+ if (!mu::png::dibToPng(m_pBMIH, pData, pRawPNG, &png_len))
+ {
+ delete[] pData;
+ delete[] pRawPNG;
+
+ return false;
+ }
+
+ // write image data to file
+ FILE* fp = _tfopen(szFileName, muT("wb"));
+
+ if (!fp)
+ {
+ delete[] pData;
+ delete[] pRawPNG;
+
+ return false;
+ }
+
+ fwrite(pRawPNG, 1, png_len, fp);
+ fclose(fp);
+
+ // free buffers
+ delete[] pRawPNG;
+ delete[] pData;
+
+ return true;
+}
diff --git a/plugins/HistoryStats/src/canvas.h b/plugins/HistoryStats/src/canvas.h
new file mode 100644
index 0000000000..6b3c40e70e
--- /dev/null
+++ b/plugins/HistoryStats/src/canvas.h
@@ -0,0 +1,57 @@
+#if !defined(HISTORYSTATS_GUARD_CANVAS_H)
+#define HISTORYSTATS_GUARD_CANVAS_H
+
+#include "_globals.h"
+
+class Canvas
+ : private pattern::NotCopyable<Canvas>
+{
+public:
+ class Digest
+ {
+ public:
+ unsigned char m_Digest[20];
+
+ Digest() { memset(m_Digest, 0, 20); }
+ Digest(const Digest& other) { memcpy(m_Digest, other.m_Digest, 20); }
+ const Digest& operator =(const Digest& other) { memcpy(m_Digest, other.m_Digest, 20); return *this; }
+
+ bool operator ==(const Digest& other) const { return (memcmp(m_Digest, other.m_Digest, 20) == 0); }
+ bool operator <(const Digest& other) const { return (memcmp(m_Digest, other.m_Digest, 20) < 0); }
+ };
+
+public:
+ static bool hasPNG() { return mu::png::_available(); }
+
+private:
+ int m_nChannels;
+ int m_nWidth;
+ int m_nHeight;
+ int m_nLineLength;
+
+ bool m_bTransColor;
+ COLORREF m_TransColor;
+
+ BITMAPINFOHEADER* m_pBMIH;
+ HDC m_hDC;
+ HBITMAP m_hBmp;
+ HGDIOBJ m_hOldBmp;
+
+private:
+ void updateTrans(BYTE* pData);
+
+public:
+ explicit Canvas(int nWidth, int nHeight);
+ ~Canvas();
+
+ void setTrans(COLORREF transColor, bool bFill = false);
+ void fillBackground(COLORREF bkColor);
+
+ HDC beginDraw();
+ void endDraw();
+
+ bool getDigest(Digest& digest);
+ bool writePNG(const mu_text* szFileName);
+};
+
+#endif // HISTORYSTATS_GUARD_CANVAS_H
diff --git a/plugins/HistoryStats/src/colbase_words.cpp b/plugins/HistoryStats/src/colbase_words.cpp
new file mode 100644
index 0000000000..9325d2a421
--- /dev/null
+++ b/plugins/HistoryStats/src/colbase_words.cpp
@@ -0,0 +1,368 @@
+#include "_globals.h"
+#include "colbase_words.h"
+
+/*
+ * ColBaseWords
+ */
+
+void ColBaseWords::addWord(WordMap* pWords, const ext::string& word, bool bOutgoing) const
+{
+ // filter words
+ if (m_bFilterWords)
+ {
+ upto_each_(i, m_ActiveWordFilter.size())
+ {
+ const Filter* pFilter = m_ActiveWordFilter[i];
+
+ switch (pFilter->getMode())
+ {
+ case Settings::fwmWordsMatching:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (word == *j)
+ {
+ return;
+ }
+ }
+ }
+ break;
+
+ case Settings::fwmWordsContaining:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (word.find(*j) != ext::string::npos)
+ {
+ return;
+ }
+ }
+ }
+ break;
+
+ case Settings::fwmWordsStartingWith:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (word.length() >= j->length() && word.substr(0, j->length()) == *j)
+ {
+ return;
+ }
+ }
+ }
+ break;
+
+ case Settings::fwmWordsEndingWith:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (word.length() >= j->length() && word.substr(word.length() - j->length(), j->length()) == *j)
+ {
+ return;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // insert, if we didn't get filtered
+ WordMap::iterator i = pWords->find(word);
+
+ if (i != pWords->end())
+ {
+ (bOutgoing ? i->second.out : i->second.in)++;
+ }
+ else
+ {
+ pWords->insert(std::make_pair(word, bOutgoing ? InOut(0, 1) : InOut(1, 0)));
+ }
+}
+
+void ColBaseWords::parseMsg(WordMap* pWords, const ext::string& msg, bool bOutgoing) const
+{
+ // filter messages
+ if (m_bFilterMessages)
+ {
+ ext::string msgLC = utils::toLowerCase(msg);
+
+ upto_each_(i, m_ActiveMessageFilter.size())
+ {
+ const Filter* pFilter = m_ActiveMessageFilter[i];
+
+ switch (pFilter->getMode())
+ {
+ case Settings::fwmMessagesMatching:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (msgLC == *j)
+ {
+ return;
+ }
+ }
+ }
+ break;
+
+ case Settings::fwmMessagesContaining:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (msgLC.find(*j) != ext::string::npos)
+ {
+ return;
+ }
+ }
+ }
+ break;
+
+ case Settings::fwmMessagesStartingWith:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (msgLC.length() >= j->length() && msgLC.substr(0, j->length()) == *j)
+ {
+ return;
+ }
+ }
+ }
+ break;
+
+ case Settings::fwmMessagesEndingWith:
+ {
+ citer_each_(WordSet, j, pFilter->getWords())
+ {
+ if (msgLC.length() >= j->length() && msgLC.substr(msgLC.length() - j->length(), j->length()) == *j)
+ {
+ return;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // start parsing into words if not already filtered
+ ext::string::size_type firstChar = 0;
+ ext::string::size_type nextSpace;
+
+ while (firstChar < msg.length() && getCharMapper()->mapChar(msg[firstChar]) == muC(' '))
+ {
+ ++firstChar;
+ }
+
+ while (firstChar < msg.length())
+ {
+ nextSpace = firstChar + 1;
+
+ while (nextSpace < msg.length() && getCharMapper()->mapChar(msg[nextSpace]) != muC(' '))
+ {
+ ++nextSpace;
+ }
+
+ int wordLen = nextSpace - firstChar;
+
+ if (wordLen >= m_nMinLength && (m_nMaxLength == 0 || wordLen <= m_nMaxLength))
+ {
+ ext::string word(wordLen, muC('_'));
+
+ upto_each_(i, wordLen)
+ {
+ word[i] = getCharMapper()->mapChar(msg[firstChar + i]);
+ }
+
+ addWord(pWords, word, bOutgoing);
+ }
+
+ firstChar = nextSpace + 1;
+
+ while (firstChar < msg.length() && getCharMapper()->mapChar(msg[firstChar]) == muC(' '))
+ {
+ ++firstChar;
+ }
+ }
+}
+
+ColBaseWords::ColBaseWords()
+ : m_nSource(2), m_nMinLength(1), m_nMaxLength(0), m_bFilterLinks(true),
+ m_hSource(NULL), m_hMinLength(NULL), m_hMaxLength(NULL), m_hFilterLinks(NULL)
+{
+}
+
+void ColBaseWords::impl_copyConfig(const Column* pSource)
+{
+ const ColBaseWords& src = *reinterpret_cast<const ColBaseWords*>(pSource);
+
+ m_nSource = src.m_nSource;
+ m_nMinLength = src.m_nMinLength;
+ m_nMaxLength = src.m_nMaxLength;
+ m_bFilterLinks = src.m_bFilterLinks;
+ m_FilterWords = src.m_FilterWords;
+}
+
+void ColBaseWords::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource, 2, 0, 2);
+ m_nMinLength = settings.readIntRanged(con::KeyMinLength, 1, 1, 1000);
+ m_nMaxLength = settings.readIntRanged(con::KeyMaxLength, 0, 0, 1000);
+ m_bFilterLinks = settings.readBool(con::KeyFilterLinks, true);
+
+ // filter words
+ m_FilterWords.clear();
+
+ int nCount = settings.readInt(con::KeyFilterWords, 0);
+
+ upto_each_(i, nCount)
+ {
+ m_FilterWords.insert(settings.readStr((con::KeyFilterWords + utils::intToString(i)).c_str(), muT("")));
+ }
+}
+
+void ColBaseWords::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeySource, m_nSource);
+ settings.writeInt (con::KeyMinLength, m_nMinLength);
+ settings.writeInt (con::KeyMaxLength, m_nMaxLength);
+ settings.writeBool(con::KeyFilterLinks, m_bFilterLinks);
+
+ // filter words
+ settings.writeInt(con::KeyFilterWords, m_FilterWords.size());
+
+ int nFilterNr = 0;
+
+ citer_each_(ColFilterSet, i, m_FilterWords)
+ {
+ settings.writeStr((con::KeyFilterWords + utils::intToString(nFilterNr++)).c_str(), i->c_str());
+ }
+}
+
+void ColBaseWords::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ OptionsCtrl::Group hTemp;
+
+ /**/hTemp = Opt.insertGroup (hGroup, i18n(muT("Extract words from")));
+ /**/ m_hSource = Opt.insertRadio (hTemp, NULL, i18n(muT("Incoming messages")));
+ /**/ Opt.insertRadio (hTemp, m_hSource, i18n(muT("Outgoing messages")));
+ /**/ Opt.insertRadio (hTemp, m_hSource, i18n(muT("All messages")));
+ /**/m_hMinLength = Opt.insertEdit (hGroup, i18n(muT("Ignore words shorter than (chars)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/m_hMaxLength = Opt.insertEdit (hGroup, i18n(muT("Ignore words longer than (chars, 0=no limit)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/m_hFilterLinks = Opt.insertCheck (hGroup, i18n(muT("Filter URLs/e-mail addresses")));
+ /**/ Opt.insertButton(hGroup, i18n(muT("Filter words/messages")), i18n(muT("Define...")), 0, Settings::biFilterWords);
+
+ Opt.setRadioChecked(m_hSource , m_nSource );
+ Opt.setEditNumber (m_hMinLength , m_nMinLength );
+ Opt.setEditNumber (m_hMaxLength , m_nMaxLength );
+ Opt.checkItem (m_hFilterLinks, m_bFilterLinks);
+}
+
+void ColBaseWords::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getRadioChecked(m_hSource );
+ m_nMinLength = Opt.getEditNumber (m_hMinLength );
+ m_nMaxLength = Opt.getEditNumber (m_hMaxLength );
+ m_bFilterLinks = Opt.isItemChecked (m_hFilterLinks);
+
+ // ensure constraints
+ utils::ensureRange(m_nMinLength, 1, 1000, 1);
+ utils::ensureRange(m_nMaxLength, 0, 1000, 0);
+}
+
+ext::string ColBaseWords::impl_contactDataGetUID() const
+{
+ ext::string strUID = ext::str(ext::format(muT("words-|-|-|-|"))
+ % m_nSource
+ % m_nMinLength
+ % m_nMaxLength
+ % (m_bFilterLinks ? 1 : 0));
+
+ citer_each_(ColFilterSet, i, m_FilterWords)
+ {
+ strUID += muT("-");
+ strUID += *i;
+ }
+
+ return strUID;
+}
+
+void ColBaseWords::impl_contactDataBeginAcquire()
+{
+ m_bFilterMessages = false;
+ m_bFilterWords = false;
+ m_ActiveMessageFilter.clear();
+ m_ActiveWordFilter.clear();
+
+ citer_each_(ColFilterSet, i, m_FilterWords)
+ {
+ const Filter* pFilter = getSettings()->getFilter(*i);
+
+ if (pFilter && !pFilter->getWords().empty())
+ {
+ switch (pFilter->getMode())
+ {
+ case Settings::fwmMessagesMatching:
+ case Settings::fwmMessagesContaining:
+ case Settings::fwmMessagesStartingWith:
+ case Settings::fwmMessagesEndingWith:
+ m_ActiveMessageFilter.push_back(pFilter);
+ m_bFilterMessages = true;
+ break;
+
+ case Settings::fwmWordsMatching:
+ case Settings::fwmWordsContaining:
+ case Settings::fwmWordsStartingWith:
+ case Settings::fwmWordsEndingWith:
+ m_ActiveWordFilter.push_back(pFilter);
+ m_bFilterWords = true;
+ break;
+ }
+ }
+ }
+}
+
+void ColBaseWords::impl_contactDataPrepare(Contact& contact) const
+{
+ WordMap* pData = new WordMap;
+
+ contact.setSlot(contactDataSlotGet(), pData);
+}
+
+void ColBaseWords::impl_contactDataFree(Contact& contact) const
+{
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+
+ if (pData)
+ {
+ delete pData;
+ contact.setSlot(contactDataSlotGet(), NULL);
+ }
+}
+
+void ColBaseWords::impl_contactDataAcquireMessage(Contact& contact, Message& msg)
+{
+ if (m_nSource == 2 || m_nSource == 1 && msg.isOutgoing() || m_nSource == 0 && !msg.isOutgoing())
+ {
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+
+ parseMsg(pData, m_bFilterLinks ? msg.getWithoutLinks() : msg.getRaw(), msg.isOutgoing());
+ }
+}
+
+void ColBaseWords::impl_contactDataMerge(Contact& contact, const Contact& include) const
+{
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+ const WordMap* pIncData = reinterpret_cast<const WordMap*>(include.getSlot(contactDataSlotGet()));
+
+ citer_each_(WordMap, j, *pIncData)
+ {
+ if (pData->find(j->first) != pData->end())
+ {
+ (*pData)[j->first] += j->second;
+ }
+ else
+ {
+ pData->insert(*j);
+ }
+ }
+}
diff --git a/plugins/HistoryStats/src/colbase_words.h b/plugins/HistoryStats/src/colbase_words.h
new file mode 100644
index 0000000000..d42803fdea
--- /dev/null
+++ b/plugins/HistoryStats/src/colbase_words.h
@@ -0,0 +1,60 @@
+#if !defined(HISTORYSTATS_GUARD_COLBASE_WORDS_H)
+#define HISTORYSTATS_GUARD_COLBASE_WORDS_H
+
+#include "column.h"
+
+class ColBaseWords
+ : public Column
+{
+public:
+ typedef std::map<ext::string, InOut> WordMap;
+ typedef Settings::WordSet WordSet;
+ typedef Settings::ColFilterSet ColFilterSet;
+ typedef Settings::Filter Filter;
+ typedef std::vector<const Filter*> FilterVecC;
+
+protected:
+ int m_nSource;
+ int m_nMinLength;
+ int m_nMaxLength;
+ bool m_bFilterLinks;
+ ColFilterSet m_FilterWords;
+
+ OptionsCtrl::Radio m_hSource;
+ OptionsCtrl::Edit m_hMinLength;
+ OptionsCtrl::Edit m_hMaxLength;
+ OptionsCtrl::Check m_hFilterLinks;
+
+ bool m_bFilterMessages;
+ bool m_bFilterWords;
+ FilterVecC m_ActiveMessageFilter;
+ FilterVecC m_ActiveWordFilter;
+
+private:
+ void addWord(WordMap* pWords, const ext::string& word, bool bOutgoing) const;
+ void parseMsg(WordMap* pWords, const ext::string& msg, bool bOutgoing) const;
+
+protected:
+ explicit ColBaseWords();
+
+protected:
+ virtual int impl_getFeatures() const { return cfHasConfig | cfAcquiresData | cfTransformsData | cfIsColBaseWords; }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual ext::string impl_contactDataGetUID() const;
+ virtual void impl_contactDataBeginAcquire();
+ virtual void impl_contactDataPrepare(Contact& contact) const;
+ virtual void impl_contactDataFree(Contact& contact) const;
+ virtual void impl_contactDataAcquireMessage(Contact& contact, Message& msg);
+ virtual void impl_contactDataMerge(Contact& contact, const Contact& include) const;
+
+public:
+ const ColFilterSet& getFilterWords() const { return m_FilterWords; }
+ void setFilterWords(const ColFilterSet& FilterWords) { m_FilterWords = FilterWords; }
+};
+
+#endif // HISTORYSTATS_GUARD_COLBASE_WORDS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column.cpp b/plugins/HistoryStats/src/column.cpp
new file mode 100644
index 0000000000..82429700bf
--- /dev/null
+++ b/plugins/HistoryStats/src/column.cpp
@@ -0,0 +1,181 @@
+#include "_globals.h"
+#include "column.h"
+
+#include "column_rank.h"
+#include "column_nick.h"
+#include "column_protocol.h"
+#include "column_group.h"
+#include "column_inout.h"
+#include "column_inoutgraph.h"
+#include "column_chatduration.h"
+#include "column_words.h"
+#include "column_wordcount.h"
+#include "column_events.h"
+#include "column_split.h"
+#include "column_timeline.h"
+#include "column_splittimeline.h"
+
+/*
+ * Column::FactoryList
+ */
+
+Column::FactoryList::FactoryList()
+{
+}
+
+Column::FactoryList::~FactoryList()
+{
+ vector_each_(i, m_List)
+ {
+ delete m_List[i].m_pFactory;
+ }
+}
+
+void Column::FactoryList::initList()
+{
+ registerUID(new Factory<ColRank> );
+ registerUID(new Factory<ColNick> );
+ registerUID(new Factory<ColProtocol> );
+ registerUID(new Factory<ColGroup> );
+ registerUID(new Factory<ColInOut> );
+ registerUID(new Factory<ColInOutGraph> );
+ registerUID(new Factory<ColChatDuration> );
+ registerUID(new Factory<ColWords> );
+ registerUID(new Factory<ColWordCount> );
+ registerUID(new Factory<ColEvents> );
+ registerUID(new Factory<ColSplit> );
+ registerUID(new Factory<ColTimeline> );
+ registerUID(new Factory<ColSplitTimeline>);
+}
+
+/*
+ * Column::IDProvider
+ */
+
+ext::string Column::IDProvider::getID()
+{
+ ext::string s = muT("q");
+ int val = m_nNextID++;
+
+ while (val > 0)
+ {
+ int digit = val % 36;
+
+ if (digit < 10)
+ {
+ s += (muC('0') + digit);
+ }
+ else
+ {
+ s += (muC('a') + digit - 10);
+ }
+
+ val /= 36;
+ }
+
+ return s;
+}
+
+/*
+ * Column
+ */
+
+Column::FactoryList Column::m_Factories;
+
+Column* Column::fromUID(const ext::string& guid)
+{
+ upto_each_(i, countColInfo())
+ {
+ if (getColInfo(i).m_UID == guid)
+ {
+ return getColInfo(i).m_pFactory->makeInstance();
+ }
+ }
+
+ return NULL;
+}
+
+void Column::registerUID(FactoryBase* pFactory)
+{
+ Column* pDummy = pFactory->makeInstance();
+
+ ColumnInfo ci;
+
+ ci.m_UID = pDummy->getUID();
+ ci.m_Title = pDummy->getTitle();
+ ci.m_Description = pDummy->getDescription();
+ ci.m_pFactory = pFactory;
+
+ m_Factories.m_List.push_back(ci);
+
+ delete pDummy;
+}
+
+void Column::writeRowspanTD(ext::ostream& tos, const ext::string& innerHTML, int row /* = 1 */, int numRows /* = 1 */, int rowSpan /* = 1 */, int colSpan /* = 1 */) const
+{
+ int curRowSpan = (row < numRows) ? 1 : (rowSpan - numRows + 1);
+
+ tos << muT("<td");
+
+ if (colSpan > 1)
+ {
+ tos << muT(" colspan=\"") << colSpan << muT("\"");
+ }
+
+ if (curRowSpan > 1)
+ {
+ tos << muT(" rowspan=\"") << curRowSpan << muT("\"");
+ }
+
+ tos << muT(">") << innerHTML << muT("</td>") << ext::endl;
+}
+
+void Column::copyAttrib(const Column* pSource)
+{
+ m_bEnabled = pSource->m_bEnabled;
+ m_CustomTitle = pSource->m_CustomTitle;
+ m_nContactDataSlot = pSource->m_nContactDataSlot;
+ m_nContactDataTransformSlot = pSource->m_nContactDataTransformSlot;
+}
+
+const ext::string Column::getCustomTitle(const ext::string& strShort, const ext::string& strLong) const
+{
+ ext::string strTitle = utils::htmlEscape(m_CustomTitle.empty() ? (m_pSettings->m_TableHeaderVerbose ? strLong : strShort) : m_CustomTitle);
+
+ if (m_pSettings->m_HeaderTooltips && (!m_pSettings->m_HeaderTooltipsIfCustom || !m_CustomTitle.empty() || (!m_pSettings->m_TableHeaderVerbose && strShort != strLong)))
+ {
+ strTitle = muT("<span title=\"") + utils::htmlEscape(strLong) + muT("\">") + strTitle + muT("</span>");
+ }
+
+ return strTitle;
+}
+
+Column* Column::clone() const
+{
+ Column* pClone = fromUID(getUID());
+
+ pClone->copyAttrib(this);
+ pClone->copyConfig(this);
+
+ return pClone;
+}
+
+void Column::outputBegin()
+{
+ int restrictions = configGetRestrictions(NULL);
+
+ m_bUsePNG =
+ m_pSettings->isPNGOutputActiveAndAvailable() && // do we want PNG output?
+ (restrictions & crPNGMask) && // has PNG capability at all?
+ !(m_pSettings->m_PNGMode == Settings::pmPreferHTML && (restrictions & crHTMLMask) == crHTMLFull) && // still prefer HTML?
+ (m_pSettings->m_PNGMode == Settings::pmEnforcePNG || (restrictions & crPNGMask) == crPNGFull); // force or fallback but with no restrictions
+
+ impl_outputBegin();
+}
+
+SIZE Column::impl_outputMeasureHeader() const
+{
+ SIZE defaultSize = { 1, 1 };
+
+ return defaultSize;
+}
diff --git a/plugins/HistoryStats/src/column.h b/plugins/HistoryStats/src/column.h
new file mode 100644
index 0000000000..74bfc2a073
--- /dev/null
+++ b/plugins/HistoryStats/src/column.h
@@ -0,0 +1,391 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_H)
+#define HISTORYSTATS_GUARD_COLUMN_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include "contact.h"
+#include "optionsctrl.h"
+#include "settingstree.h"
+#include "statistic.h"
+#include "message.h"
+
+#include <vector>
+
+/*
+ * Column
+ */
+
+class Column
+ : private pattern::NotCopyable<Column>
+{
+private:
+ class FactoryBase
+ {
+ public:
+ virtual Column* makeInstance() = 0;
+ };
+
+ template<typename T_>
+ class Factory
+ : public FactoryBase
+ {
+ public:
+ virtual Column* makeInstance() { return new T_; }
+ };
+
+ class ColumnInfo
+ {
+ public:
+ ext::string m_UID;
+ const mu_text* m_Title;
+ const mu_text* m_Description;
+ FactoryBase* m_pFactory;
+ };
+
+ class FactoryList
+ {
+ public:
+ std::vector<ColumnInfo> m_List;
+
+ public:
+ FactoryList();
+ ~FactoryList();
+ void initList();
+ };
+
+public:
+ class IDProvider
+ {
+ private:
+ int m_nNextID;
+
+ public:
+ IDProvider() : m_nNextID(1) { }
+ ext::string getID();
+ };
+
+private:
+ static FactoryList m_Factories;
+
+public:
+ static void registerColumns() { m_Factories.initList(); }
+ static Column* fromUID(const ext::string& guid);
+ static void registerUID(FactoryBase* pFactory);
+ static int countColInfo() { return m_Factories.m_List.size(); }
+ static const ColumnInfo& getColInfo(int index) { return m_Factories.m_List[index]; }
+
+ static bool inRange(int nValue, int nMin, int nMax) { return (nValue >= nMin && nValue <= nMax); }
+
+public:
+ enum DisplayType {
+ asContact,
+ asOmitted,
+ asTotal,
+ };
+
+ enum ColumnFlags {
+ cfAcquiresData = 0x01,
+ cfTransformsData = 0x02,
+ cfHasConfig = 0x04,
+ // internal, a bit hackish
+ cfIsColBaseWords = 0x08,
+ };
+
+ enum ConfigRestrictions {
+ // supported HTML output modes
+ crHTMLPartial = 0x01, // MEMO: never implement columns with only partial HTML support (only full or none)
+ crHTMLFull = 0x03, // includes crHTMPPartial
+ crHTMLMask = 0x0F,
+
+ // supported PNG output modes
+ crPNGPartial = 0x10,
+ crPNGFull = 0x30, // includes crPNGPartial
+ crPNGMask = 0xF0,
+
+ // valid column restrictions:
+ // - crHTMLFull
+ // - crHTMLFull | crPNGPartial
+ // - crHTMLFull | crPNGFull
+ // - crPNGFull
+ };
+
+ typedef std::pair<ext::string, ext::string> StylePair;
+ typedef std::vector<StylePair> StyleList;
+
+private:
+ int m_nContactDataSlot;
+ int m_nContactDataTransformSlot;
+
+ bool m_bEnabled;
+ ext::string m_CustomTitle;
+
+ Statistic* m_pStatistic;
+ Settings* m_pSettings;
+ Settings::CharMapper* m_pCharMapper;
+
+ bool m_bUsePNG;
+
+protected:
+ /*
+ * Renders a TD with row "row" out of "numRows" having the information
+ * about total rowspan "rowSpan" and colspan "colSpan".
+ */
+ void writeRowspanTD(ext::ostream& tos, const ext::string& innerHTML, int row = 1, int numRows = 1, int rowSpan = 1, int colSpan = 1) const;
+
+ void copyAttrib(const Column* pSource);
+
+public:
+ Column()
+ : m_bEnabled(true), m_CustomTitle(muT("")),
+ m_nContactDataSlot(-1), m_nContactDataTransformSlot(-1),
+ m_pStatistic(NULL), m_pSettings(NULL), m_pCharMapper(NULL),
+ m_bUsePNG(false)
+ {
+ }
+ virtual ~Column() { }
+
+ void contactDataSlotAssign(int contactDataSlot) { m_nContactDataSlot = contactDataSlot; }
+ int contactDataSlotGet() const { return m_nContactDataSlot; }
+ void contactDataTransformSlotAssign(int contactDataTransformSlot) { m_nContactDataTransformSlot = contactDataTransformSlot; }
+ int contactDataTransformSlotGet() const { return m_nContactDataTransformSlot; }
+
+ void setEnabled(bool bEnable) { m_bEnabled = bEnable; }
+ bool isEnabled() const { return m_bEnabled; }
+ void setCustomTitle(const ext::string& customTitle) { m_CustomTitle = customTitle; }
+ const ext::string getCustomTitle() const { return m_CustomTitle; }
+ const ext::string getCustomTitle(const ext::string& strShort, const ext::string& strLong) const;
+ ext::string getTitleForOptions() { return m_CustomTitle.empty() ? getTitle() : (m_CustomTitle + muT(" (") + getTitle() + muT(")")); }
+
+ void setHelpers(Statistic* pStatistic, Settings* pSettings, Settings::CharMapper* pCharMapper) { m_pStatistic = pStatistic; m_pSettings = pSettings; m_pCharMapper = pCharMapper; }
+ Statistic* getStatistic() const { return m_pStatistic; }
+ const Settings* getSettings() const { return m_pSettings; }
+ const Settings::CharMapper* getCharMapper() const { return m_pCharMapper; }
+
+ bool usePNG() { return m_bUsePNG; }
+
+ Column* clone() const;
+
+public:
+ /*
+ * public interface for virtual functions
+ */
+ const mu_text* getUID() const { return impl_getUID(); }
+ const mu_text* getTitle() const { return impl_getTitle(); }
+ const mu_text* getDescription() const { return impl_getDescription(); }
+ void copyConfig(const Column* pSource) { impl_copyConfig(pSource); }
+ int getFeatures() const { return impl_getFeatures(); }
+ void configRead(const SettingsTree& settings) { impl_configRead(settings); }
+ void configWrite(SettingsTree& settings) const { impl_configWrite(settings); }
+ void configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup) { impl_configToUI(Opt, hGroup); }
+ void configFromUI(OptionsCtrl& Opt) { impl_configFromUI(Opt); }
+ int configGetRestrictions(ext::string* pDetails) const { return impl_configGetRestrictions(pDetails); }
+ ext::string contactDataGetUID() const { return impl_contactDataGetUID(); }
+ void contactDataBeginAcquire() { impl_contactDataBeginAcquire(); }
+ void contactDataEndAcquire() { impl_contactDataEndAcquire(); }
+ void contactDataPrepare(Contact& contact) const { impl_contactDataPrepare(contact); }
+ void contactDataFree(Contact& contact) const { impl_contactDataFree(contact); }
+ void contactDataAcquireMessage(Contact& contact, Message& msg) { impl_contactDataAcquireMessage(contact, msg); }
+ void contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration) { impl_contactDataAcquireChat(contact, bOutgoing, localTimestampStarted, duration); }
+ void contactDataMerge(Contact& contact, const Contact& include) const { impl_contactDataMerge(contact, include); }
+ void contactDataTransform(Contact& contact) const { impl_contactDataTransform(contact); }
+ void contactDataTransformCleanup(Contact& contact) const { impl_contactDataTransformCleanup(contact); }
+ void columnDataBeforeOmit() { impl_columnDataBeforeOmit(); }
+ void columnDataAfterOmit() { impl_columnDataAfterOmit(); }
+ StyleList outputGetAdditionalStyles(IDProvider& idp) { return impl_outputGetAdditionalStyles(idp); }
+ SIZE outputMeasureHeader() const { return impl_outputMeasureHeader(); }
+ void outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const { impl_outputRenderHeader(tos, row, rowSpan); }
+ void outputBegin();
+ void outputEnd() { impl_outputEnd(); }
+ void outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display) { impl_outputRenderRow(tos, contact, display); }
+
+protected:
+ /*** VIRTUAL/ABSTRACT *** GLOBAL ***/
+
+ /*
+ * Returns a unique ID for column.
+ * [virtual/abstract]
+ */
+ virtual const mu_text* impl_getUID() const = 0;
+
+ /*
+ * Returns the title for the column.
+ * [virtual/abstract]
+ */
+ virtual const mu_text* impl_getTitle() const = 0;
+
+ /*
+ * Returns the description for the column.
+ * [virtual/abstract]
+ */
+ virtual const mu_text* impl_getDescription() const = 0;
+
+ /*
+ * Creates a exact copy of the column.
+ * [virtual/default: copy nothing]
+ */
+ virtual void impl_copyConfig(const Column* pSource) { }
+
+ /*
+ * Queries for column's features.
+ * [virtual/abstract]
+ */
+ virtual int impl_getFeatures() const = 0;
+
+ /*** VIRTUAL/ABSTRACT *** CONFIGURATION ***/
+
+ /*
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_configRead(const SettingsTree& settings) { }
+
+ /*
+ * [vurtual/default: do nothing]
+ */
+ virtual void impl_configWrite(SettingsTree& settings) const { }
+
+ /*
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup) { }
+
+ /*
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_configFromUI(OptionsCtrl& Opt) { }
+
+ /*
+ * Check if current column options imply output restrictions.
+ * [virtual/abstract]
+ */
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const = 0;
+
+ /*** VIRTUAL/ABSTRACT *** PER-CONTACT DATA ACQUISITION ***/
+
+ /*
+ * Returns a unique ID for the type of additional per-contact
+ * data collected by this column (for sharing data between columns).
+ * Ignores, if column doesn't acquire any data.
+ * [virtual/default: empty string]
+ */
+ virtual ext::string impl_contactDataGetUID() const { return ext::string(); }
+
+ /*
+ * Perform initialization of column-global data before acquisition.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataBeginAcquire() { }
+
+ /*
+ * Perform finalization of column-global data after acuqisition.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataEndAcquire() { }
+
+ /*
+ * Initializes data structures for acquiring additional per-contact
+ * data for given contact. Works on previously defined slot.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataPrepare(Contact& contact) const { }
+
+ /*
+ * Frees all data structures associated with this column for the
+ * given contact. Works on previously defined slot.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataFree(Contact& contact) const { }
+
+ /*
+ * Acquires data for this column and for the given contact. Works
+ * on previously defined slot.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataAcquireMessage(Contact& contact, Message& msg) { }
+
+ /*
+ * Acquires data for this column and for the given contact. Works
+ * on previously defined slot.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration) { }
+
+ /*** VIRTUAL/ABSTRACT *** DATA POSTPROCESSING ***/
+
+ /*
+ * Merges additonal per-contact data for two contacts. Not called, if
+ * column doesn't acquire any data.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataMerge(Contact& contact, const Contact& include) const { }
+
+ /*
+ * Perform any post processing for additional per-contact data. Will
+ * be called after merge but before sort.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataTransform(Contact& contact) const { }
+
+ /*
+ * Perform cleanup after post processing.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_contactDataTransformCleanup(Contact& contact) const { }
+
+ /*** VIRTUAL/ABSTRACT *** COLUMN SPECIFIC GLOBAL DATA ***/
+
+ /*
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_columnDataBeforeOmit() { }
+
+ /*
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_columnDataAfterOmit() { }
+
+ /*** VIRTUAL/ABSTRACT *** OUTPUT ***/
+
+ /*
+ * Returns additional (i.e. non-default) styles required by this
+ * column as a list of CSS selectors (first) and CSS styles (second).
+ * [virtual/default: empty list]
+ */
+ virtual StyleList impl_outputGetAdditionalStyles(IDProvider& idp) { return StyleList(); }
+
+ /*
+ * Returns number of columns and table rows (for header only) used by
+ * this column.
+ * [virtual/default: 1 row and 1 column]
+ */
+ virtual SIZE impl_outputMeasureHeader() const;
+
+ /*
+ * Renders the row "row" of the header to the output stream "tos". The
+ * parameter "rowSpan" specifies the previously calculated number of
+ * total header rowspan.
+ * [virtual/abstract]
+ */
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const = 0;
+
+ /*
+ * Perform column-global initialization before output.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_outputBegin() { }
+
+ /*
+ * Perform column-global finalization before output.
+ * [virtual/default: do nothing]
+ */
+ virtual void impl_outputEnd() { }
+
+ /*
+ * Renders the given contact "contact" to the output stream "tos" and
+ * modfies rendering depending on value in "display".
+ * [virtual/abstract]
+ */
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display) = 0;
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_H
diff --git a/plugins/HistoryStats/src/column_chatduration.cpp b/plugins/HistoryStats/src/column_chatduration.cpp
new file mode 100644
index 0000000000..b21dc451b4
--- /dev/null
+++ b/plugins/HistoryStats/src/column_chatduration.cpp
@@ -0,0 +1,257 @@
+#include "_globals.h"
+#include "column_chatduration.h"
+
+/*
+ * ColChatDuration
+ */
+
+ColChatDuration::ColChatDuration()
+ : m_nVisMode(3), m_bGraph(true), m_bDetail(true),
+ m_hVisMode(NULL), m_hGraph(NULL), m_hDetail(NULL)
+{
+}
+
+void ColChatDuration::impl_copyConfig(const Column* pSource)
+{
+ const ColChatDuration& src = *reinterpret_cast<const ColChatDuration*>(pSource);
+
+ m_nVisMode = src.m_nVisMode;
+ m_bGraph = src.m_bGraph;
+ m_bDetail = src.m_bDetail;
+}
+
+void ColChatDuration::impl_configRead(const SettingsTree& settings)
+{
+ m_nVisMode = settings.readIntRanged(con::KeyVisMode, 3, 0, 3);
+ m_bGraph = settings.readBool (con::KeyGraph , true);
+ m_bDetail = settings.readBool (con::KeyDetail , true);
+}
+
+void ColChatDuration::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeyVisMode, m_nVisMode);
+ settings.writeBool(con::KeyGraph , m_bGraph );
+ settings.writeBool(con::KeyDetail , m_bDetail );
+}
+
+void ColChatDuration::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ OptionsCtrl::Item hTemp;
+
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("Chat duration type")));
+ /**/ m_hVisMode = Opt.insertRadio(hTemp, NULL, i18n(muT("Minimum")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Average")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Maximum")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Total (sum of all chats)")));
+ /**/m_hGraph = Opt.insertCheck(hGroup, i18n(muT("Show bar graph for chat duration type")));
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("Other information in tooltip")));
+
+ Opt.setRadioChecked(m_hVisMode, m_nVisMode);
+ Opt.checkItem (m_hGraph , m_bGraph );
+ Opt.checkItem (m_hDetail , m_bDetail );
+}
+
+void ColChatDuration::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nVisMode = Opt.getRadioChecked(m_hVisMode);
+ m_bGraph = Opt.isItemChecked (m_hGraph );
+ m_bDetail = Opt.isItemChecked (m_hDetail );
+}
+
+Column::StyleList ColChatDuration::impl_outputGetAdditionalStyles(IDProvider& idp)
+{
+ StyleList l;
+
+ if (m_bGraph)
+ {
+ m_CSS = idp.getID();
+
+ l.push_back(StylePair(muT("td.") + m_CSS, muT("vertical-align: middle; padding: 2px 2px 2px 2px;")));
+ l.push_back(StylePair(muT("td.") + m_CSS + muT(" div.n"), muT("text-align: center;")));
+
+ if (!usePNG())
+ {
+ l.push_back(StylePair(muT("div.") + m_CSS, muT("position: relative; left: 50%; margin-left: -35px; width: 70px; height: 15px; background-color: ") + utils::colorToHTML(con::ColorBarBack) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div"), muT("position: absolute; top: 0px; left: 0px; height: 15px; overflow: hidden; background-color: ") + utils::colorToHTML(con::ColorBar) + muT(";")));
+ }
+ }
+
+ return l;
+}
+
+void ColChatDuration::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szVisModeDesc[] = {
+ I18N(muT("Minimum chat duration")),
+ I18N(muT("Average chat duration")),
+ I18N(muT("Maximum chat duration")),
+ I18N(muT("Total chat duration")),
+ };
+
+ if (row == 1)
+ {
+ writeRowspanTD(tos, getCustomTitle(i18n(muT("Chat duration")), i18n(szVisModeDesc[m_nVisMode])) + (m_bGraph ? muT("<div style=\"width: 70px;\"></div>") : muT("")), row, 1, rowSpan);
+ }
+}
+
+void ColChatDuration::impl_columnDataAfterOmit()
+{
+ // AFTER, i.e. contacts are trimmed to what user will see
+
+ if (m_bGraph)
+ {
+ static DWORD (Contact::*getChatDurX[4])() const = {
+ &Contact::getChatDurMin,
+ &Contact::getChatDurAvg,
+ &Contact::getChatDurMax,
+ &Contact::getChatDurSum,
+ };
+
+ m_nMaxForGraph = 1;
+
+ upto_each_(i, getStatistic()->countContacts())
+ {
+ const Contact& cur = getStatistic()->getContact(i);
+
+ if (cur.isChatDurValid())
+ {
+ m_nMaxForGraph = max(m_nMaxForGraph, (cur.*getChatDurX[m_nVisMode])());
+ }
+ }
+
+ if (m_nVisMode != 3)
+ {
+ if (getStatistic()->hasOmitted() && getStatistic()->getOmitted().isChatDurValid())
+ {
+ m_nMaxForGraph = max(m_nMaxForGraph, (getStatistic()->getOmitted().*getChatDurX[m_nVisMode])());
+ }
+
+ if (getStatistic()->hasTotals() && getStatistic()->getTotals().isChatDurValid())
+ {
+ m_nMaxForGraph = max(m_nMaxForGraph, (getStatistic()->getTotals().*getChatDurX[m_nVisMode])());
+ }
+ }
+ }
+}
+
+void ColChatDuration::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ static DWORD (Contact::*getChatDurX[4])() const = {
+ &Contact::getChatDurMin,
+ &Contact::getChatDurAvg,
+ &Contact::getChatDurMax,
+ &Contact::getChatDurSum,
+ };
+
+ // begin output
+ if (m_bGraph)
+ {
+ tos << muT("<td class=\"") << m_CSS;
+ }
+ else
+ {
+ tos << muT("<td class=\"num");
+ }
+
+ // read and format data
+ ext::string strAll[4] = {
+ i18n(muT("(unknown)")), // min
+ i18n(muT("(unknown)")), // avg
+ i18n(muT("(unknown)")), // max
+ utils::durationToString(contact.getChatDurSum()), // sum
+ };
+
+ if (contact.isChatDurValid())
+ {
+ strAll[0] = utils::durationToString(contact.getChatDurMin());
+ strAll[1] = utils::durationToString(contact.getChatDurAvg());
+ strAll[2] = utils::durationToString(contact.getChatDurMax());
+ }
+
+ // output tooltip
+ if (m_bDetail)
+ {
+ static const mu_text* szPrefixes[] = {
+ I18N(muT("[Min] #{amount}")),
+ I18N(muT("[Avg] #{amount}")),
+ I18N(muT("[Max] #{amount}")),
+ I18N(muT("[Sum] #{amount}")),
+ };
+
+ ext::string strTooltip;
+ int nSegments = 0;
+
+ upto_each_(i, 4)
+ {
+ if (i != m_nVisMode)
+ {
+ strTooltip += ext::str(ext::kformat(i18n(szPrefixes[i])) % muT("#{amount}") * strAll[i]);
+ ++nSegments;
+
+ if (nSegments < 3)
+ {
+ strTooltip += muT(" / ");
+ }
+ }
+ }
+
+ tos << muT("\" title=\"") << utils::htmlEscape(strTooltip) << muT("\">");
+ }
+ else
+ {
+ tos << muT("\">");
+ }
+
+ if (m_bGraph)
+ {
+ tos << muT("<div class=\"n\">")
+ << utils::htmlEscape(strAll[m_nVisMode]);
+
+ if (display == asContact || m_nVisMode != 3)
+ {
+ int barW = static_cast<int>(70.0 * (contact.*getChatDurX[m_nVisMode])() / m_nMaxForGraph);
+
+ if (m_nVisMode != 3 && !contact.isChatDurValid())
+ {
+ barW = 0;
+ }
+
+ if (usePNG())
+ {
+ // draw graph
+ Canvas canvas(70, 15);
+
+ canvas.fillBackground(con::ColorBarBack);
+
+ HDC hDC = canvas.beginDraw();
+
+ SetBkColor(hDC, con::ColorBar);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(0, 0, barW, 15), NULL, 0, NULL);
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<br/><img src=\"") << strFinalFile << muT("\"/>");
+ }
+ }
+ else
+ {
+ tos << muT("</div>")
+ << muT("<div class=\"") << m_CSS << muT("\">")
+ << muT("<div style=\"width: ") << barW << muT("px;\"></div>");
+ }
+ }
+
+ tos << muT("</div>");
+ }
+ else
+ {
+ tos << utils::htmlEscape(strAll[m_nVisMode]);
+ }
+
+ tos << muT("</td>") << ext::endl;
+}
diff --git a/plugins/HistoryStats/src/column_chatduration.h b/plugins/HistoryStats/src/column_chatduration.h
new file mode 100644
index 0000000000..dd68efffbc
--- /dev/null
+++ b/plugins/HistoryStats/src/column_chatduration.h
@@ -0,0 +1,46 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_CHATDURATION_H)
+#define HISTORYSTATS_GUARD_COLUMN_CHATDURATION_H
+
+#include "column.h"
+
+/*
+ * ColChatDuration
+ */
+
+class ColChatDuration
+ : public Column
+{
+private:
+ int m_nVisMode;
+ bool m_bGraph;
+ bool m_bDetail;
+
+ OptionsCtrl::Radio m_hVisMode;
+ OptionsCtrl::Check m_hGraph;
+ OptionsCtrl::Check m_hDetail;
+
+ DWORD m_nMaxForGraph;
+
+ ext::string m_CSS;
+
+public:
+ explicit ColChatDuration();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColChatDuration; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Chat duration")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding the amount of time you have chatted with the given contact.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull | (m_bGraph ? crPNGFull : 0); }
+ virtual void impl_columnDataAfterOmit();
+ virtual StyleList impl_outputGetAdditionalStyles(IDProvider& idp);
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_CHATDURATION_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_events.cpp b/plugins/HistoryStats/src/column_events.cpp
new file mode 100644
index 0000000000..2186f8bb91
--- /dev/null
+++ b/plugins/HistoryStats/src/column_events.cpp
@@ -0,0 +1,92 @@
+#include "_globals.h"
+#include "column_events.h"
+
+/*
+ * ColEvents
+ */
+
+ColEvents::ColEvents()
+ : m_nSource(5), m_hSource(NULL)
+{
+}
+
+void ColEvents::impl_copyConfig(const Column* pSource)
+{
+ const ColEvents& src = *reinterpret_cast<const ColEvents*>(pSource);
+
+ m_nSource = src.m_nSource;
+}
+
+void ColEvents::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource, 5, 0, 5);
+}
+
+void ColEvents::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt(con::KeySource, m_nSource);
+}
+
+void ColEvents::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ m_hSource = Opt.insertCombo(hGroup, i18n(muT("Events to count")));
+
+ static const mu_text* sourceTexts[] = {
+ I18N(muT("URLs (incoming)")),
+ I18N(muT("URLs (outgoing)")),
+ I18N(muT("URLs (all)")),
+ I18N(muT("Files (incoming)")),
+ I18N(muT("Files (outgoing)")),
+ I18N(muT("Files (all)")),
+ };
+
+ array_each_(i, sourceTexts)
+ {
+ Opt.addComboItem(m_hSource, i18n(sourceTexts[i]));
+ }
+
+ Opt.setComboSelected(m_hSource, m_nSource);
+}
+
+void ColEvents::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getComboSelected(m_hSource);
+}
+
+void ColEvents::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szShortDesc[] = {
+ I18N(muT("URLs")),
+ I18N(muT("Files")),
+ };
+
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("Incoming URLs")),
+ I18N(muT("Outgoing URLs")),
+ I18N(muT("URLs")),
+ I18N(muT("Incoming files")),
+ I18N(muT("Outgoing files")),
+ I18N(muT("Files")),
+ };
+
+ if (row == 1)
+ {
+ writeRowspanTD(tos, getCustomTitle(i18n(szShortDesc[m_nSource / 3]), i18n(szSourceDesc[m_nSource])), row, 1, rowSpan);
+ }
+}
+
+void ColEvents::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ static int (Contact::*getData[6])() const = {
+ &Contact::getInUrls,
+ &Contact::getOutUrls,
+ &Contact::getTotalUrls,
+ &Contact::getInFiles,
+ &Contact::getOutFiles,
+ &Contact::getTotalFiles,
+ };
+
+ tos << muT("<td class=\"num\">")
+ << utils::intToGrouped((contact.*getData[m_nSource])())
+ << muT("</td>") << ext::endl;
+}
diff --git a/plugins/HistoryStats/src/column_events.h b/plugins/HistoryStats/src/column_events.h
new file mode 100644
index 0000000000..6a3aa9f827
--- /dev/null
+++ b/plugins/HistoryStats/src/column_events.h
@@ -0,0 +1,36 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_EVENTS_H)
+#define HISTORYSTATS_GUARD_COLUMN_EVENTS_H
+
+#include "column.h"
+
+/*
+ * ColEvents
+ */
+
+class ColEvents
+ : public Column
+{
+private:
+ int m_nSource;
+
+ OptionsCtrl::Radio m_hSource;
+
+public:
+ explicit ColEvents();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColEvents; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Events")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding event counts for incoming, outgoing or total number of files or URLs.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_EVENTS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_group.cpp b/plugins/HistoryStats/src/column_group.cpp
new file mode 100644
index 0000000000..9d816af1c4
--- /dev/null
+++ b/plugins/HistoryStats/src/column_group.cpp
@@ -0,0 +1,31 @@
+#include "_globals.h"
+#include "column_group.h"
+
+/*
+ * ColGroup
+ */
+
+void ColGroup::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ if (row == 1)
+ {
+ writeRowspanTD(tos, getCustomTitle(i18n(muT("Group")), i18n(muT("Group"))), row, 1, rowSpan);
+ }
+}
+
+void ColGroup::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ if (display == asContact)
+ {
+ ext::string groupName = contact.getGroup();
+
+ // replace subgroup separator with something better (really better?)
+ utils::replaceAllInPlace(groupName, muT("\\"), muT(" > "));
+
+ tos << muT("<td>") << utils::htmlEscape(groupName) << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td>&nbsp;</td>");
+ }
+}
diff --git a/plugins/HistoryStats/src/column_group.h b/plugins/HistoryStats/src/column_group.h
new file mode 100644
index 0000000000..f8c68c704d
--- /dev/null
+++ b/plugins/HistoryStats/src/column_group.h
@@ -0,0 +1,23 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_GROUP_H)
+#define HISTORYSTATS_GUARD_COLUMN_GROUP_H
+
+#include "column.h"
+
+/*
+ * ColumnGroup
+ */
+
+class ColGroup
+ : public Column
+{
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColGroup; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Group")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding the contact list's group name the contact is in.")); }
+ virtual int impl_getFeatures() const { return 0; }
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_GROUP_H
diff --git a/plugins/HistoryStats/src/column_inout.cpp b/plugins/HistoryStats/src/column_inout.cpp
new file mode 100644
index 0000000000..4aaf5f1743
--- /dev/null
+++ b/plugins/HistoryStats/src/column_inout.cpp
@@ -0,0 +1,167 @@
+#include "_globals.h"
+#include "column_inout.h"
+
+/*
+ * ColInOut
+ */
+
+ColInOut::ColInOut()
+ : m_nSource(2), m_bAbsolute(false), m_nAbsTime(1),
+ m_hSource(NULL), m_hAbsolute(NULL), m_hAbsTime(NULL)
+{
+}
+
+void ColInOut::impl_copyConfig(const Column* pSource)
+{
+ const ColInOut& src = *reinterpret_cast<const ColInOut*>(pSource);
+
+ m_nSource = src.m_nSource;
+ m_bAbsolute = src.m_bAbsolute;
+ m_nAbsTime = src.m_nAbsTime;
+}
+
+void ColInOut::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource , 2, 0, 8);
+ m_bAbsolute = settings.readBool (con::KeyAbsolute, false);
+ m_nAbsTime = settings.readIntRanged(con::KeyAbsTime , 1, 0, 2);
+}
+
+void ColInOut::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeySource , m_nSource );
+ settings.writeBool(con::KeyAbsolute, m_bAbsolute);
+ settings.writeInt (con::KeyAbsTime , m_nAbsTime );
+}
+
+void ColInOut::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ OptionsCtrl::Group hTemp;
+
+ /**/m_hSource = Opt.insertCombo(hGroup, i18n(muT("Data source")));
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("Display as")));
+ /**/ m_hAbsolute = Opt.insertRadio(hTemp, NULL, i18n(muT("Absolute")));
+ /**/ m_hAbsolute = Opt.insertRadio(hTemp, m_hAbsolute, i18n(muT("Average")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hAbsTime = Opt.insertRadio(m_hAbsolute, NULL, i18n(muT("Units per day")));
+ /**/ Opt.insertRadio(m_hAbsolute, m_hAbsTime, i18n(muT("Units per week")));
+ /**/ Opt.insertRadio(m_hAbsolute, m_hAbsTime, i18n(muT("Units per month (30 days)")));
+
+ static const mu_text* sourceTexts[] = {
+ I18N(muT("Characters (incoming)")),
+ I18N(muT("Characters (outgoing)")),
+ I18N(muT("Characters (all)")),
+ I18N(muT("Messages (incoming)")),
+ I18N(muT("Messages (outgoing)")),
+ I18N(muT("Messages (all)")),
+ I18N(muT("Chats (incoming)")),
+ I18N(muT("Chats (outgoing)")),
+ I18N(muT("Chats (all)")),
+ };
+
+ array_each_(i, sourceTexts)
+ {
+ Opt.addComboItem(m_hSource, i18n(sourceTexts[i]));
+ }
+
+ Opt.setComboSelected(m_hSource , m_nSource );
+ Opt.setRadioChecked (m_hAbsolute, m_bAbsolute ? 0 : 1);
+ Opt.setRadioChecked (m_hAbsTime , m_nAbsTime );
+}
+
+void ColInOut::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getComboSelected(m_hSource);
+ m_bAbsolute = (Opt.getRadioChecked(m_hAbsolute) == 0);
+ m_nAbsTime = Opt.getRadioChecked(m_hAbsTime);
+}
+
+void ColInOut::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szShortDesc[] = {
+ I18N(muT("Characters")),
+ I18N(muT("Messages")),
+ I18N(muT("Chats"))
+ };
+
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("Incoming characters")),
+ I18N(muT("Outgoing characters")),
+ I18N(muT("Characters")),
+ I18N(muT("Incoming messages")),
+ I18N(muT("Outgoing messages")),
+ I18N(muT("Messages")),
+ I18N(muT("Incoming chats")),
+ I18N(muT("Outgoing chats")),
+ I18N(muT("Chats")),
+ };
+
+ static const mu_text* szUnitDesc[] = {
+ I18N(muT("day")),
+ I18N(muT("week")),
+ I18N(muT("month")),
+ };
+
+ if (row == 1)
+ {
+ ext::string strTitle;
+
+ if (m_bAbsolute)
+ {
+ strTitle = i18n(szSourceDesc[m_nSource]);
+ }
+ else
+ {
+ strTitle = str(ext::kformat(i18n(muT("#{data} per #{unit}")))
+ % muT("#{data}") * i18n(szSourceDesc[m_nSource])
+ % muT("#{unit}") * i18n(szUnitDesc[m_nAbsTime]));
+ }
+
+ writeRowspanTD(tos, getCustomTitle(i18n(szShortDesc[m_nSource / 3]), strTitle), row, 1, rowSpan);
+ }
+}
+
+void ColInOut::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ if (m_bAbsolute)
+ {
+ static int (Contact::*getData[])() const = {
+ &Contact::getInBytes,
+ &Contact::getOutBytes,
+ &Contact::getTotalBytes,
+ &Contact::getInMessages,
+ &Contact::getOutMessages,
+ &Contact::getTotalMessages,
+ &Contact::getInChats,
+ &Contact::getOutChats,
+ &Contact::getTotalChats,
+ };
+
+ tos << muT("<td class=\"num\">")
+ << utils::intToGrouped((contact.*getData[m_nSource])())
+ << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ static double (Contact::*getData[])() const = {
+ &Contact::getInBytesAvg,
+ &Contact::getOutBytesAvg,
+ &Contact::getTotalBytesAvg,
+ &Contact::getInMessagesAvg,
+ &Contact::getOutMessagesAvg,
+ &Contact::getTotalMessagesAvg,
+ &Contact::getInChatsAvg,
+ &Contact::getOutChatsAvg,
+ &Contact::getTotalChatsAvg,
+ };
+
+ static const double avgFactor[] = {
+ 60.0 * 60.0 * 24.0,
+ 60.0 * 60.0 * 24.0 * 7.0,
+ 60.0 * 60.0 * 24.0 * 30.0,
+ };
+
+ tos << muT("<td class=\"num\">")
+ << utils::floatToGrouped((contact.*getData[m_nSource])() * avgFactor[m_nAbsTime], 1)
+ << muT("</td>") << ext::endl;
+ }
+}
diff --git a/plugins/HistoryStats/src/column_inout.h b/plugins/HistoryStats/src/column_inout.h
new file mode 100644
index 0000000000..ac66377312
--- /dev/null
+++ b/plugins/HistoryStats/src/column_inout.h
@@ -0,0 +1,40 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_INOUT_H)
+#define HISTORYSTATS_GUARD_COLUMN_INOUT_H
+
+#include "column.h"
+
+/*
+ * ColInOut
+ */
+
+class ColInOut
+ : public Column
+{
+private:
+ int m_nSource;
+ bool m_bAbsolute;
+ int m_nAbsTime;
+
+ OptionsCtrl::Combo m_hSource;
+ OptionsCtrl::Radio m_hAbsolute;
+ OptionsCtrl::Radio m_hAbsTime;
+
+public:
+ explicit ColInOut();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColInOut; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("In/out")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding counts for incoming, outgoing or total characters, messages or chats. This column can display absolute and average values.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_INOUT_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_inoutgraph.cpp b/plugins/HistoryStats/src/column_inoutgraph.cpp
new file mode 100644
index 0000000000..a28c4b7fd5
--- /dev/null
+++ b/plugins/HistoryStats/src/column_inoutgraph.cpp
@@ -0,0 +1,356 @@
+#include "_globals.h"
+#include "column_inoutgraph.h"
+
+/*
+ * ColInOutGraph
+ */
+
+ColInOutGraph::ColInOutGraph()
+ : m_nSource(0), m_bAbsolute(false), m_nAbsTime(1), m_bShowSum(true),
+ m_bDetail(true), m_bDetailPercent(false), m_bDetailInvert(false), m_bGraphPercent(true),
+ m_hSource(NULL), m_hAbsolute(NULL), m_hAbsTime(NULL), m_hShowSum(NULL),
+ m_hDetail(NULL), m_hDetailPercent(NULL), m_hDetailInvert(NULL), m_hGraphPercent(NULL)
+{
+}
+
+void ColInOutGraph::impl_copyConfig(const Column* pSource)
+{
+ const ColInOutGraph& src = *reinterpret_cast<const ColInOutGraph*>(pSource);
+
+ m_nSource = src.m_nSource;
+ m_bAbsolute = src.m_bAbsolute;
+ m_nAbsTime = src.m_nAbsTime;
+ m_bShowSum = src.m_bShowSum;
+ m_bDetail = src.m_bDetail;
+ m_bDetailPercent = src.m_bDetailPercent;
+ m_bDetailInvert = src.m_bDetailInvert;
+ m_bGraphPercent = src.m_bGraphPercent;
+}
+
+void ColInOutGraph::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource , 0, 0, 2);
+ m_bAbsolute = settings.readBool (con::KeyAbsolute , false);
+ m_nAbsTime = settings.readIntRanged(con::KeyAbsTime , 1, 0, 2);
+ m_bShowSum = settings.readBool (con::KeyShowSum , true);
+ m_bDetail = settings.readBool (con::KeyDetail , true);
+ m_bDetailPercent = settings.readBool (con::KeyDetailPercent, false);
+ m_bDetailInvert = settings.readBool (con::KeyDetailInvert , false);
+ m_bGraphPercent = settings.readBool (con::KeyGraphPercent , true);
+}
+
+void ColInOutGraph::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeySource , m_nSource );
+ settings.writeBool(con::KeyAbsolute , m_bAbsolute );
+ settings.writeInt (con::KeyAbsTime , m_nAbsTime );
+ settings.writeBool(con::KeyShowSum , m_bShowSum );
+ settings.writeBool(con::KeyDetail , m_bDetail );
+ settings.writeBool(con::KeyDetailPercent, m_bDetailPercent);
+ settings.writeBool(con::KeyDetailInvert , m_bDetailInvert );
+ settings.writeBool(con::KeyGraphPercent , m_bGraphPercent );
+}
+
+void ColInOutGraph::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ OptionsCtrl::Group hTemp;
+
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("Data source")));
+ /**/ m_hSource = Opt.insertRadio(hTemp, NULL, i18n(muT("Characters")));
+ /**/ Opt.insertRadio(hTemp, m_hSource, i18n(muT("Messages")));
+ /**/ Opt.insertRadio(hTemp, m_hSource, i18n(muT("Chats")));
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("Display as")));
+ /**/ m_hAbsolute = Opt.insertRadio(hTemp, NULL, i18n(muT("Absolute")));
+ /**/ m_hAbsolute = Opt.insertRadio(hTemp, m_hAbsolute, i18n(muT("Average")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hAbsTime = Opt.insertRadio(m_hAbsolute, NULL, i18n(muT("Units per day")));
+ /**/ Opt.insertRadio(m_hAbsolute, m_hAbsTime, i18n(muT("Units per week")));
+ /**/ Opt.insertRadio(m_hAbsolute, m_hAbsTime, i18n(muT("Units per month (30 days)")));
+ /**/m_hShowSum = Opt.insertCheck(hGroup, i18n(muT("Show sum of incoming and outgoing")));
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("In/out details (tooltip)")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hDetailPercent = Opt.insertCheck(m_hDetail, i18n(muT("Percentage in tooltip")));
+ /**/ m_hDetailInvert = Opt.insertCheck(m_hDetail, i18n(muT("Absolute in tooltip if average selected and vice versa")));
+ /**/m_hGraphPercent = Opt.insertCheck(hGroup, i18n(muT("Percentage in bar graph")));
+
+ Opt.setRadioChecked(m_hSource , m_nSource );
+ Opt.setRadioChecked(m_hAbsolute , m_bAbsolute ? 0 : 1);
+ Opt.setRadioChecked(m_hAbsTime , m_nAbsTime );
+ Opt.checkItem (m_hShowSum , m_bShowSum );
+ Opt.checkItem (m_hDetail , m_bDetail );
+ Opt.checkItem (m_hDetailPercent, m_bDetailPercent );
+ Opt.checkItem (m_hDetailInvert , m_bDetailInvert );
+ Opt.checkItem (m_hGraphPercent , m_bGraphPercent );
+}
+
+void ColInOutGraph::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getRadioChecked(m_hSource);
+ m_bAbsolute = (Opt.getRadioChecked(m_hAbsolute) == 0);
+ m_nAbsTime = Opt.getRadioChecked(m_hAbsTime);
+ m_bShowSum = Opt.isItemChecked(m_hShowSum);
+ m_bDetail = Opt.isItemChecked(m_hDetail);
+ m_bDetailPercent = Opt.isItemChecked(m_hDetailPercent);
+ m_bDetailInvert = Opt.isItemChecked(m_hDetailInvert);
+ m_bGraphPercent = Opt.isItemChecked(m_hGraphPercent);
+}
+
+Column::StyleList ColInOutGraph::impl_outputGetAdditionalStyles(IDProvider& idp)
+{
+ StyleList l;
+
+ m_CSS = idp.getID();
+
+ l.push_back(StylePair(muT("td.") + m_CSS, muT("vertical-align: middle; padding: 2px 2px 2px 2px;")));
+
+ if (m_bShowSum)
+ {
+ l.push_back(StylePair(muT("td.") + m_CSS + muT(" div.n"), muT("text-align: center;")));
+ }
+
+ if (!usePNG())
+ {
+ l.push_back(StylePair(muT("div.") + m_CSS, muT("position: relative; left: 50%; margin-left: -50px; width: 100px; height: 15px; background-color: ") + utils::colorToHTML(con::ColorBack) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div"), muT("position: absolute; top: 0px; height: 15px; overflow: hidden; font-size: 80%; color: ") + utils::colorToHTML(con::ColorBack) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div.obar"), muT("left: 0px; background-color: ") + utils::colorToHTML(con::ColorOut) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div.ibar"), muT("right: 0px; background-color: ") + utils::colorToHTML(con::ColorIn) + muT(";")));
+
+ if (m_bGraphPercent)
+ {
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div.otext"), muT("left: 2px; width: 48px; text-align: left; z-index: 9;")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div.itext"), muT("right: 2px; width: 48px; text-align: right; z-index: 9;")));
+ }
+ }
+
+ return l;
+}
+
+SIZE ColInOutGraph::impl_outputMeasureHeader() const
+{
+ SIZE colSize = { 2, 2 };
+
+ return colSize;
+}
+
+void ColInOutGraph::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("Characters")),
+ I18N(muT("Messages")),
+ I18N(muT("Chats"))
+ };
+
+ static const mu_text* szSourceUnit[] = {
+ I18N(muT("day")),
+ I18N(muT("week")),
+ I18N(muT("month")),
+ };
+
+ if (row == 1)
+ {
+ ext::string strTitle;
+
+ if (m_bAbsolute)
+ {
+ strTitle = i18n(szSourceDesc[m_nSource]);
+ }
+ else
+ {
+ strTitle = str(ext::kformat(i18n(muT("#{data} per #{unit}")))
+ % muT("#{data}") * i18n(szSourceDesc[m_nSource])
+ % muT("#{unit}") * i18n(szSourceUnit[m_nAbsTime]));
+ }
+
+ writeRowspanTD(tos, getCustomTitle(i18n(szSourceDesc[m_nSource]), strTitle) + muT("<div style=\"width: 100px;\"></div>"), 1, 2, rowSpan, 2);
+ }
+ else if (row == 2)
+ {
+ writeRowspanTD(tos, i18n(muT("Outgoing")), 2, 2, rowSpan);
+ writeRowspanTD(tos, i18n(muT("Incoming")), 2, 2, rowSpan);
+ }
+}
+
+void ColInOutGraph::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ // fetch absolute values
+ static int (Contact::*getOut[3])() const = { &Contact::getOutBytes, &Contact::getOutMessages, &Contact::getOutChats };
+ static int (Contact::*getIn[3])() const = { &Contact::getInBytes, &Contact::getInMessages, &Contact::getInChats };
+
+ int numOut = (contact.*getOut[m_nSource])();
+ int numIn = (contact.*getIn[m_nSource])();
+ int numTotal = numOut + numIn;
+
+ // fetch average values
+ static double (Contact::*getAvgOut[3])() const = { &Contact::getOutBytesAvg, &Contact::getOutMessagesAvg, &Contact::getOutChatsAvg };
+ static double (Contact::*getAvgIn[3])() const = { &Contact::getInBytesAvg, &Contact::getInMessagesAvg, &Contact::getInChatsAvg };
+ static double (Contact::*getAvgTotal[3])() const = { &Contact::getTotalBytesAvg, &Contact::getTotalMessagesAvg, &Contact::getTotalChatsAvg };
+
+ static const double avgFactor[] = {
+ 60.0 * 60.0 * 24.0,
+ 60.0 * 60.0 * 24.0 * 7.0,
+ 60.0 * 60.0 * 24.0 * 30.0,
+ };
+
+ double avgOut = avgFactor[m_nAbsTime] * (contact.*getAvgOut[m_nSource])();
+ double avgIn = avgFactor[m_nAbsTime] * (contact.*getAvgIn[m_nSource])();
+ double avgTotal = avgFactor[m_nAbsTime] * (contact.*getAvgTotal[m_nSource])();
+
+ // begin output
+ tos << muT("<td colspan=\"2\" class=\"") << m_CSS;
+
+ if (m_bDetail)
+ {
+ ext::string strOut, strIn;
+
+ if ((m_bAbsolute && !m_bDetailInvert) || (!m_bAbsolute && m_bDetailInvert))
+ {
+ strOut = utils::intToGrouped(numOut);
+ strIn = utils::intToGrouped(numIn);
+ }
+ else
+ {
+ strOut = utils::floatToGrouped(avgOut, 1);
+ strIn = utils::floatToGrouped(avgIn, 1);
+ }
+
+ if (m_bDetailPercent)
+ {
+ tos << muT("\" title=\"")
+ << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[Out] #{out_amount} (#{out_ratio}) / [In] #{in_amount} (#{in_ratio})")))
+ % muT("#{out_amount}") * strOut
+ % muT("#{out_ratio}") * utils::ratioToPercent(numOut, numTotal)
+ % muT("#{in_amount}") * strIn
+ % muT("#{in_ratio}") * utils::ratioToPercent(numIn, numTotal)))
+ << muT("\">");
+ }
+ else
+ {
+ tos << muT("\" title=\"")
+ << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[Out] #{out_amount} / [In] #{in_amount}")))
+ % muT("#{out_amount}") * strOut
+ % muT("#{in_amount}") * strIn))
+ << muT("\">");
+ }
+ }
+ else
+ {
+ tos << muT("\">");
+ }
+
+ if (numOut + numIn == 0)
+ {
+ numOut = numIn = 1;
+ }
+
+ int allNum = numIn + numOut;
+ int outW = (int) (99.0 * numOut / allNum);
+ int inW = 99 - outW;
+
+ if (outW == 99)
+ {
+ outW++;
+ }
+
+ if (inW == 99)
+ {
+ inW++;
+ }
+
+ if (usePNG())
+ {
+ tos << muT("<div class=\"n\">");
+
+ if (m_bShowSum)
+ {
+ tos << (m_bAbsolute ? utils::intToGrouped(numTotal) : utils::floatToGrouped(avgTotal, 1))
+ << muT("<br/>");
+ }
+
+ // draw graph
+ Canvas canvas(100, 15);
+
+ canvas.fillBackground(con::ColorBack);
+
+ HDC hDC = canvas.beginDraw();
+
+ SetBkColor(hDC, con::ColorOut);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(0, 0, outW, 15), NULL, 0, NULL);
+
+ SetBkColor(hDC, con::ColorIn);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(100 - inW, 0, 100, 15), NULL, 0, NULL);
+
+ if (m_bGraphPercent)
+ {
+ SetBkMode(hDC, TRANSPARENT);
+ SetTextColor(hDC, con::ColorBack);
+
+ RECT r = { 2, 0, 98, 14 };
+
+ LOGFONT lf = {
+ -MulDiv(8, GetDeviceCaps(hDC, LOGPIXELSY), 72),
+ 0,
+ 0,
+ 0,
+ FW_NORMAL,
+ FALSE,
+ FALSE,
+ FALSE,
+ ANSI_CHARSET,
+ OUT_DEFAULT_PRECIS,
+ CLIP_DEFAULT_PRECIS,
+ DEFAULT_QUALITY,
+ DEFAULT_PITCH | FF_SWISS,
+ muT("Verdana")
+ };
+
+ HFONT hFont = CreateFontIndirect(&lf);
+ HGDIOBJ hOldFont = SelectObject(hDC, hFont);
+
+ DrawText(hDC, utils::ratioToPercent(numOut, allNum).c_str(), -1, &r, DT_LEFT | DT_VCENTER | DT_NOPREFIX | DT_SINGLELINE);
+ DrawText(hDC, utils::ratioToPercent(numIn, allNum).c_str(), -1, &r, DT_RIGHT | DT_VCENTER | DT_NOPREFIX | DT_SINGLELINE);
+
+ SelectObject(hDC, hOldFont);
+ }
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<img src=\"") << strFinalFile << muT("\"/>");
+ }
+
+ tos << muT("</div>");
+ }
+ else
+ {
+ if (m_bShowSum)
+ {
+ tos << muT("<div class=\"n\">")
+ << (m_bAbsolute ? utils::intToGrouped(numTotal) : utils::floatToGrouped(avgTotal, 1))
+ << muT("</div>");
+ }
+
+ tos << muT("<div class=\"") << m_CSS << muT("\">");
+
+ if (outW != 0)
+ {
+ tos << muT("<div class=\"obar\" style=\"width: ") << outW << muT("px;\"></div>");
+ }
+
+ if (inW != 0)
+ {
+ tos << muT("<div class=\"ibar\" style=\"width: ") << inW << muT("px;\"></div>");
+ }
+
+ if (m_bGraphPercent)
+ {
+ tos << muT("<div class=\"otext\">") << utils::ratioToPercent(numOut, allNum) << muT("</div>");
+ tos << muT("<div class=\"itext\">") << utils::ratioToPercent(numIn, allNum) << muT("</div>");
+ }
+
+ tos << muT("</div>");
+ }
+
+ tos << muT("</td>") << ext::endl;
+}
diff --git a/plugins/HistoryStats/src/column_inoutgraph.h b/plugins/HistoryStats/src/column_inoutgraph.h
new file mode 100644
index 0000000000..875a09d004
--- /dev/null
+++ b/plugins/HistoryStats/src/column_inoutgraph.h
@@ -0,0 +1,54 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_INOUTGRAPH_H)
+#define HISTORYSTATS_GUARD_COLUMN_INOUTGRAPH_H
+
+#include "column.h"
+
+/*
+ * ColumnInOut
+ */
+
+class ColInOutGraph
+ : public Column
+{
+private:
+ int m_nSource;
+ bool m_bAbsolute;
+ int m_nAbsTime;
+ bool m_bShowSum;
+ bool m_bDetail;
+ bool m_bDetailPercent;
+ bool m_bDetailInvert;
+ bool m_bGraphPercent;
+
+ OptionsCtrl::Radio m_hSource;
+ OptionsCtrl::Radio m_hAbsolute;
+ OptionsCtrl::Radio m_hAbsTime;
+ OptionsCtrl::Check m_hShowSum;
+ OptionsCtrl::Check m_hDetail;
+ OptionsCtrl::Check m_hDetailPercent;
+ OptionsCtrl::Check m_hDetailInvert;
+ OptionsCtrl::Check m_hGraphPercent;
+
+ ext::string m_CSS;
+
+public:
+ ColInOutGraph();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColInOutGraph; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("In/out graph")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding in/out bar graphs for characters, messages or chats.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull | crPNGFull; }
+ virtual StyleList impl_outputGetAdditionalStyles(IDProvider& idp);
+ virtual SIZE impl_outputMeasureHeader() const;
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_INOUTGRAPH_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_nick.cpp b/plugins/HistoryStats/src/column_nick.cpp
new file mode 100644
index 0000000000..f61744a545
--- /dev/null
+++ b/plugins/HistoryStats/src/column_nick.cpp
@@ -0,0 +1,114 @@
+#include "_globals.h"
+#include "column_nick.h"
+
+/*
+ * ColNick
+ */
+
+ColNick::ColNick()
+ : m_bDetail(true)
+ , m_bContactCount(true)
+ , m_hDetail(NULL)
+ , m_hContactCount(NULL)
+{
+}
+
+void ColNick::impl_copyConfig(const Column* pSource)
+{
+ const ColNick& src = *reinterpret_cast<const ColNick*>(pSource);
+
+ m_bDetail = src.m_bDetail;
+ m_bContactCount = src.m_bContactCount;
+}
+
+void ColNick::impl_configRead(const SettingsTree& settings)
+{
+ m_bDetail = settings.readBool(con::KeyDetail, true);
+ m_bContactCount = settings.readBool(con::KeyContactCount, true);
+}
+
+void ColNick::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeBool(con::KeyDetail, m_bDetail);
+ settings.writeBool(con::KeyContactCount, m_bContactCount);
+}
+
+void ColNick::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("First/last message time (tooltip)")));
+ /**/m_hContactCount = Opt.insertCheck(hGroup, i18n(muT("Show countact count for omitted/totals (tooltip)")));
+
+ Opt.checkItem(m_hDetail , m_bDetail );
+ Opt.checkItem(m_hContactCount, m_bContactCount);
+}
+
+void ColNick::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_bDetail = Opt.isItemChecked(m_hDetail);
+ m_bContactCount = Opt.isItemChecked(m_hContactCount);
+}
+
+void ColNick::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ if (row == 1)
+ {
+ writeRowspanTD(tos, getCustomTitle(i18n(muT("Nick")), i18n(muT("Nick"))), row, 1, rowSpan);
+ }
+}
+
+void ColNick::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ tos << muT("<td");
+
+ if (m_bDetail || (m_bContactCount && display != asContact))
+ {
+ ext::string strFirstTime = i18n(muT("(unknown)"));
+ ext::string strLastTime = i18n(muT("(unknown)"));
+
+ if (contact.isFirstLastTimeValid())
+ {
+ strFirstTime = utils::timestampToDateTime(contact.getFirstTime());
+ strLastTime = utils::timestampToDateTime(contact.getLastTime());
+ }
+
+ ext::string strTooltip;
+
+ if (m_bDetail && !(m_bContactCount && display != asContact))
+ {
+ strTooltip = utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[First] #{first_time} / [Last] #{last_time}")))
+ % muT("#{first_time}") * strFirstTime
+ % muT("#{last_time}") * strLastTime));
+ }
+ else if (m_bDetail && (m_bContactCount && display != asContact))
+ {
+ strTooltip = utils::htmlEscape(ext::str(ext::kformat(i18n(muT("#{count} contacts / [First] #{first_time} / [Last] #{last_time}")))
+ % muT("#{count}") * contact.getNumContacts()
+ % muT("#{first_time}") * strFirstTime
+ % muT("#{last_time}") * strLastTime));
+ }
+ else // if (!m_bDetail && (m_bContactCount && display != asContact))
+ {
+ strTooltip = utils::htmlEscape(ext::str(ext::kformat(i18n(muT("#{count} contacts")))
+ % muT("#{count}") * contact.getNumContacts()));
+ }
+
+ tos << muT(" title=\"") << strTooltip << muT("\">");
+ }
+ else
+ {
+ tos << muT(">");
+ }
+
+ if (display == asContact)
+ {
+ tos << utils::htmlEscape(contact.getNick()) << muT("</td>") << ext::endl;
+ }
+ else if (display == asOmitted)
+ {
+ tos << i18n(muT("Omitted")) << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << i18n(muT("Totals")) << muT("</td>") << ext::endl;
+ }
+}
diff --git a/plugins/HistoryStats/src/column_nick.h b/plugins/HistoryStats/src/column_nick.h
new file mode 100644
index 0000000000..81f7905c93
--- /dev/null
+++ b/plugins/HistoryStats/src/column_nick.h
@@ -0,0 +1,38 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_NICK_H)
+#define HISTORYSTATS_GUARD_COLUMN_NICK_H
+
+#include "column.h"
+
+/*
+ * ColNick
+ */
+
+class ColNick
+ : public Column
+{
+private:
+ bool m_bDetail;
+ bool m_bContactCount;
+
+ OptionsCtrl::Check m_hDetail;
+ OptionsCtrl::Check m_hContactCount;
+
+public:
+ explicit ColNick();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColNick; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Nick")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding the contact's nick and first/last message time if selected.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_NICK_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_protocol.cpp b/plugins/HistoryStats/src/column_protocol.cpp
new file mode 100644
index 0000000000..bbbe67ba01
--- /dev/null
+++ b/plugins/HistoryStats/src/column_protocol.cpp
@@ -0,0 +1,26 @@
+#include "_globals.h"
+#include "column_protocol.h"
+
+/*
+ * ColProtocol
+ */
+
+void ColProtocol::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ if (row == 1)
+ {
+ writeRowspanTD(tos, getCustomTitle(i18n(muT("Protocol")), i18n(muT("Protocol"))), row, 1, rowSpan);
+ }
+}
+
+void ColProtocol::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ if (display == asContact)
+ {
+ tos << muT("<td>") << utils::htmlEscape(contact.getProtocol()) << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td>&nbsp;</td>");
+ }
+}
diff --git a/plugins/HistoryStats/src/column_protocol.h b/plugins/HistoryStats/src/column_protocol.h
new file mode 100644
index 0000000000..b846243275
--- /dev/null
+++ b/plugins/HistoryStats/src/column_protocol.h
@@ -0,0 +1,24 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_PROTOCOL_H)
+#define HISTORYSTATS_GUARD_COLUMN_PROTOCOL_H
+
+#include "column.h"
+
+/*
+ * ColProtocol
+ */
+
+class ColProtocol
+ : public Column
+{
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColProtocol; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Protocol")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding the contact's protocol.")); }
+ virtual int impl_getFeatures() const { return 0; }
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+
+#endif // HISTORYSTATS_GUARD_COLUMN_PROTOCOL_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_rank.cpp b/plugins/HistoryStats/src/column_rank.cpp
new file mode 100644
index 0000000000..ae44689be9
--- /dev/null
+++ b/plugins/HistoryStats/src/column_rank.cpp
@@ -0,0 +1,33 @@
+#include "_globals.h"
+#include "column_rank.h"
+
+/*
+ * ColRank
+ */
+
+void ColRank::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ if (row == 1)
+ {
+ writeRowspanTD(tos, getCustomTitle(i18n(muT("Rank")), i18n(muT("Rank"))), row, 1, rowSpan);
+ }
+}
+
+void ColRank::impl_outputBegin()
+{
+ m_nNextRank = 1;
+}
+
+void ColRank::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ if (display == asContact)
+ {
+ tos << muT("<td class=\"num\">")
+ << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("#{rank}."))) % muT("#{rank}") * (m_nNextRank++)))
+ << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td>&nbsp;</td>");
+ }
+}
diff --git a/plugins/HistoryStats/src/column_rank.h b/plugins/HistoryStats/src/column_rank.h
new file mode 100644
index 0000000000..29d737277f
--- /dev/null
+++ b/plugins/HistoryStats/src/column_rank.h
@@ -0,0 +1,27 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_RANK_H)
+#define HISTORYSTATS_GUARD_COLUMN_RANK_H
+
+#include "column.h"
+
+/*
+ * ColRank
+ */
+
+class ColRank
+ : public Column
+{
+private:
+ int m_nNextRank;
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColRank; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Rank")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding the contact's placing according to your sorting criteria.")); }
+ virtual int impl_getFeatures() const { return 0; }
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crHTMLFull; }
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputBegin();
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_RANK_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_split.cpp b/plugins/HistoryStats/src/column_split.cpp
new file mode 100644
index 0000000000..bba1625f2b
--- /dev/null
+++ b/plugins/HistoryStats/src/column_split.cpp
@@ -0,0 +1,463 @@
+#include "_globals.h"
+#include "column_split.h"
+
+/*
+ * ColSplit
+ */
+
+ColSplit::ColSplit()
+ : m_nSource(0), m_nSourceType(2), m_nVisMode(0),
+ m_nBlockUnit(0), m_nUnitsPerBlock(6), m_nBlocks(28), m_nGraphAlign(1),
+ m_bDetail(true),
+ m_hSource(NULL), m_hVisMode(NULL),
+ m_hBlockUnit(NULL), m_hUnitsPerBlock(NULL), m_hBlocks(NULL), m_hGraphAlign(NULL),
+ m_hDetail(NULL),
+ m_nTimeDiv(3600), m_nTimeMod(24), m_nTimeOffset(0)
+{
+}
+
+void ColSplit::impl_copyConfig(const Column* pSource)
+{
+ const ColSplit& src = *reinterpret_cast<const ColSplit*>(pSource);
+
+ m_nSource = src.m_nSource;
+ m_nSourceType = src.m_nSourceType;
+ m_nVisMode = src.m_nVisMode;
+ m_nBlockUnit = src.m_nBlockUnit;
+ m_nUnitsPerBlock = src.m_nUnitsPerBlock;
+ m_nBlocks = src.m_nBlocks;
+ m_nGraphAlign = src.m_nGraphAlign;
+ m_bDetail = src.m_bDetail;
+}
+
+void ColSplit::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource, 0, 0, 2);
+ m_nSourceType = settings.readIntRanged(con::KeySourceType, 2, 0, 2);
+ m_nVisMode = settings.readIntRanged(con::KeyVisMode, 0, 0, 1);
+ m_nBlockUnit = settings.readIntRanged(con::KeyBlockUnit, 0, 0, 2);
+ m_nUnitsPerBlock = settings.readIntRanged(con::KeyUnitsPerBlock, 6, 1, 100);
+ m_nBlocks = settings.readIntRanged(con::KeyBlocks, 28, 1, 49);
+ m_nGraphAlign = settings.readIntRanged(con::KeyGraphAlign, 1, 0, 1);
+ m_bDetail = settings.readBool(con::KeyDetail, true);
+}
+
+void ColSplit::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeySource, m_nSource);
+ settings.writeInt (con::KeySourceType, m_nSourceType);
+ settings.writeInt (con::KeyVisMode, m_nVisMode);
+ settings.writeInt (con::KeyBlockUnit, m_nBlockUnit);
+ settings.writeInt (con::KeyUnitsPerBlock, m_nUnitsPerBlock);
+ settings.writeInt (con::KeyBlocks, m_nBlocks);
+ settings.writeInt (con::KeyGraphAlign, m_nGraphAlign);
+ settings.writeBool(con::KeyDetail, m_bDetail);
+}
+
+void ColSplit::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ OptionsCtrl::Group hTemp;
+ OptionsCtrl::Radio hTempRadio;
+
+ /**/m_hSource = Opt.insertCombo(hGroup, i18n(muT("Data source")));
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("\"Split\" type")));
+ /**/ m_hVisMode = Opt.insertRadio(hTemp, NULL, i18n(muT("Hours of day")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Days of week")));
+ /**/ hTempRadio = Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Custom (for experts only)")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ hTemp = Opt.insertGroup(hTempRadio, i18n(muT("Column setup")));
+ /**/ m_hBlockUnit = Opt.insertCombo(hTemp, i18n(muT("Bar unit")));
+ /**/ m_hUnitsPerBlock = Opt.insertEdit (hTemp, i18n(muT("Units per bar")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hBlocks = Opt.insertEdit (hTemp, i18n(muT("Bars per graph")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ hTemp = Opt.insertGroup(hTempRadio, i18n(muT("Graph alignment")));
+ /**/ m_hGraphAlign = Opt.insertRadio(hTemp, NULL, i18n(muT("Align on day boundary")));
+ /**/ Opt.insertRadio(hTemp, m_hGraphAlign, i18n(muT("Align on week boundary")));
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("Details for every bar (tooltip)")));
+
+ static const mu_text* sourceTexts[] = {
+ I18N(muT("Characters (incoming)")),
+ I18N(muT("Characters (outgoing)")),
+ I18N(muT("Characters (all)")),
+ I18N(muT("Messages (incoming)")),
+ I18N(muT("Messages (outgoing)")),
+ I18N(muT("Messages (all)")),
+ I18N(muT("Chats (incoming)")),
+ I18N(muT("Chats (outgoing)")),
+ I18N(muT("Chats (all)")),
+ };
+
+ array_each_(i, sourceTexts)
+ {
+ Opt.addComboItem(m_hSource, i18n(sourceTexts[i]));
+ }
+
+ static const mu_text* unitTexts[] = {
+ I18N(muT("Hours")),
+ I18N(muT("Days")),
+ I18N(muT("Weeks")),
+ };
+
+ array_each_(i, unitTexts)
+ {
+ Opt.addComboItem(m_hBlockUnit, i18n(unitTexts[i]));
+ }
+
+ Opt.setComboSelected(m_hSource , 3 * m_nSource + m_nSourceType);
+ Opt.setRadioChecked (m_hVisMode , m_nVisMode );
+ Opt.setComboSelected(m_hBlockUnit , m_nBlockUnit );
+ Opt.setEditNumber (m_hUnitsPerBlock, m_nUnitsPerBlock );
+ Opt.setEditNumber (m_hBlocks , m_nBlocks );
+ Opt.setRadioChecked (m_hGraphAlign , m_nGraphAlign );
+ Opt.checkItem (m_hDetail , m_bDetail );
+}
+
+void ColSplit::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getComboSelected(m_hSource ) / 3;
+ m_nSourceType = Opt.getComboSelected(m_hSource ) % 3;
+ m_nVisMode = Opt.getRadioChecked (m_hVisMode );
+ m_nBlockUnit = Opt.getComboSelected(m_hBlockUnit );
+ m_nUnitsPerBlock = Opt.getEditNumber (m_hUnitsPerBlock);
+ m_nBlocks = Opt.getEditNumber (m_hBlocks );
+ m_nGraphAlign = Opt.getRadioChecked (m_hGraphAlign );
+ m_bDetail = Opt.isItemChecked (m_hDetail );
+
+ // ensure constraints
+ utils::ensureRange(m_nUnitsPerBlock, 1, 100, 6);
+ utils::ensureRange(m_nBlocks, 1, 49, 28);
+}
+
+int ColSplit::impl_configGetRestrictions(ext::string* pDetails) const
+{
+ if (pDetails && m_bDetail)
+ {
+ *pDetails = i18n(muT("Details for every bar (tooltip) are only available with HTML output."));
+ }
+
+ // m_bDetail "on" means we need tooltips and they are not available with PNG output
+ return crHTMLFull | (m_bDetail ? crPNGPartial : crPNGFull);
+}
+
+ext::string ColSplit::impl_contactDataGetUID() const
+{
+ SplitParams params = getParams();
+
+ return ext::str(ext::format(muT("split-|-|-|-|-|"))
+ % m_nSource
+ % m_nSourceType
+ % params.hours_in_block
+ % params.blocks_in_column
+ % params.alignment);
+}
+
+void ColSplit::impl_contactDataBeginAcquire()
+{
+ SplitParams params = getParams();
+
+ m_nTimeDiv = 3600 * params.hours_in_block;
+ m_nTimeMod = params.blocks_in_column;
+
+ if (params.alignment == 1)
+ {
+ DWORD dwOffset = 0;
+ tm offsetTM = *gmtime(reinterpret_cast<const time_t*>(&dwOffset));
+
+ m_nTimeOffset = 86400 * ((offsetTM.tm_wday + 6) % 7);
+ }
+ else
+ {
+ m_nTimeOffset = 0;
+ }
+}
+
+void ColSplit::impl_contactDataPrepare(Contact& contact) const
+{
+ SplitParams params = getParams();
+
+ int* pData = new int[params.blocks_in_column];
+
+ upto_each_(i, params.blocks_in_column)
+ {
+ pData[i] = 0;
+ }
+
+ contact.setSlot(contactDataSlotGet(), pData);
+}
+
+void ColSplit::impl_contactDataFree(Contact& contact) const
+{
+ int* pData = reinterpret_cast<int*>(contact.getSlot(contactDataSlotGet()));
+
+ if (pData)
+ {
+ delete[] pData;
+ contact.setSlot(contactDataSlotGet(), NULL);
+ }
+}
+
+void ColSplit::addToSlot(Contact& contact, DWORD localTimestamp, int toAdd)
+{
+ if (toAdd > 0)
+ {
+ int* pData = reinterpret_cast<int*>(contact.getSlot(contactDataSlotGet()));
+
+ pData[((localTimestamp + m_nTimeOffset) / m_nTimeDiv) % m_nTimeMod] += toAdd;
+ }
+}
+
+void ColSplit::impl_contactDataAcquireMessage(Contact& contact, Message& msg)
+{
+ if (!msg.isOutgoing() && m_nSourceType == 0 || msg.isOutgoing() && m_nSourceType == 1 || m_nSourceType == 2)
+ {
+ if (m_nSource == 0)
+ {
+ addToSlot(contact, msg.getTimestamp(), msg.getLength());
+ }
+ else if (m_nSource == 1)
+ {
+ addToSlot(contact, msg.getTimestamp(), 1);
+ }
+ }
+}
+
+void ColSplit::impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration)
+{
+ if (m_nSource == 2 && (!bOutgoing && m_nSourceType == 0 || bOutgoing && m_nSourceType == 1 || m_nSourceType == 2))
+ {
+ addToSlot(contact, localTimestampStarted, 1);
+ }
+}
+
+void ColSplit::impl_contactDataMerge(Contact& contact, const Contact& include) const
+{
+ SplitParams params = getParams();
+
+ int* pData = reinterpret_cast<int*>(contact.getSlot(contactDataSlotGet()));
+ const int* pIncData = reinterpret_cast<const int*>(include.getSlot(contactDataSlotGet()));
+
+ upto_each_(i, params.blocks_in_column)
+ {
+ pData[i] += pIncData[i];
+ }
+}
+
+Column::StyleList ColSplit::impl_outputGetAdditionalStyles(IDProvider& idp)
+{
+ StyleList l;
+
+ if (!usePNG())
+ {
+ SplitParams params = getParams();
+
+ m_CSS = idp.getID();
+
+ l.push_back(StylePair(
+ muT("div.") + m_CSS,
+ ext::str(ext::format(muT("position: relative; left: 50%; margin-left: -|px; width: |px; height: 50px;"))
+ % ((5 * params.blocks_in_column - 1) / 2)
+ % (5 * params.blocks_in_column - 1))));
+
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div"), muT("position: absolute; top: 0px; width: 4px; height: 50px; overflow: hidden;")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div div"), muT("position: absolute; left: 0px; width: 4px; height: 50px; background-color: ") + utils::colorToHTML(con::ColorBar) + muT(";")));
+ }
+
+ return l;
+}
+
+void ColSplit::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szTypeDesc[] = {
+ I18N(muT("Hours of day")),
+ I18N(muT("Days of week")),
+ I18N(muT("\"Split\"")),
+ };
+
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("incoming characters")),
+ I18N(muT("outgoing characters")),
+ I18N(muT("all characters")),
+ I18N(muT("incoming messages")),
+ I18N(muT("outgoing messages")),
+ I18N(muT("all messages")),
+ I18N(muT("incoming chats")),
+ I18N(muT("outgoing chats")),
+ I18N(muT("all chats")),
+ };
+
+ if (row == 1)
+ {
+ SplitParams params = getParams();
+ ext::string strTitle = str(ext::kformat(i18n(muT("#{type} for #{data}")))
+ % muT("#{type}") * i18n(szTypeDesc[params.effective_vis_mode])
+ % muT("#{data}") * i18n(szSourceDesc[3 * m_nSource + m_nSourceType]));
+
+ writeRowspanTD(tos, getCustomTitle(i18n(szTypeDesc[params.effective_vis_mode]), strTitle) + ext::str(ext::format(muT("<div style=\"width: |px;\"></div>")) % (5 * params.blocks_in_column - 1)), row, 1, rowSpan);
+ }
+}
+
+ColSplit::SplitParams ColSplit::getParams() const
+{
+ static const int unitFactors[] = { 1, 24, 168 };
+
+ SplitParams params;
+
+ params.effective_vis_mode = m_nVisMode;
+
+ switch (m_nVisMode)
+ {
+ case 0: // hours of day
+ {
+ params.alignment = 0;
+ params.hours_in_block = 1;
+ params.blocks_in_column = 24;
+ }
+ break;
+
+ case 1: // days of week
+ {
+ params.alignment = 1;
+ params.hours_in_block = 24;
+ params.blocks_in_column = 7;
+ }
+ break;
+
+ case 2: // custom
+ {
+ params.alignment = m_nGraphAlign;
+ params.hours_in_block = unitFactors[m_nBlockUnit] * m_nUnitsPerBlock;
+ params.blocks_in_column = m_nBlocks;
+
+ // heuristics for custom mode
+ if (params.hours_in_block == 1 && params.blocks_in_column == 24)
+ {
+ params.effective_vis_mode = 0;
+ }
+ else if (params.hours_in_block == 24 && params.blocks_in_column == 7 && params.alignment == 1)
+ {
+ params.effective_vis_mode = 1;
+ }
+ }
+ break;
+ }
+
+ return params;
+}
+
+void ColSplit::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ SplitParams params = getParams();
+
+ static const mu_text* szWDayName[] = {
+ I18N(muT("wday3:Mon")),
+ I18N(muT("wday3:Tue")),
+ I18N(muT("wday3:Wed")),
+ I18N(muT("wday3:Thu")),
+ I18N(muT("wday3:Fri")),
+ I18N(muT("wday3:Sat")),
+ I18N(muT("wday3:Sun")),
+ };
+
+ const int* pData = reinterpret_cast<const int*>(contact.getSlot(contactDataSlotGet()));
+
+ int top = 0;
+
+ upto_each_(j, params.blocks_in_column)
+ {
+ top = max(top, pData[j]);
+ }
+
+ if (top == 0)
+ {
+ top = 1;
+ }
+
+ if (usePNG())
+ {
+ tos << muT("<td class=\"img_bottom\">");
+
+ // draw graph
+ Canvas canvas(5 * params.blocks_in_column - 1, 50);
+
+ canvas.setTrans(con::ColorBack, true);
+
+ HDC hDC = canvas.beginDraw();
+
+ SetBkColor(hDC, con::ColorBar);
+
+ upto_each_(j, params.blocks_in_column)
+ {
+ int part_top = pData[j];
+
+ if (part_top != 0)
+ {
+ int bar_len = (50 * part_top + top - 1) / top;
+
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(j * 5, 50 - bar_len, j * 5 + 4, 50), NULL, 0, NULL);
+ }
+ }
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<img src=\"") << strFinalFile << muT("\"/>");
+ }
+
+ tos << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td class=\"bars_bottom\">")
+ << muT("<div class=\"") << m_CSS << muT("\">") << ext::endl;
+
+ upto_each_(j, params.blocks_in_column)
+ {
+ int part_top = pData[j];
+
+ if (m_bDetail)
+ {
+ ext::string divTitle;
+
+ if (params.effective_vis_mode == 0)
+ {
+ divTitle = ext::str(ext::kformat(i18n(muT("[#{hour}:00-#{hour}:59] #{amount}")))
+ % muT("#{hour}") * utils::intToPadded(j, 2)
+ % muT("#{amount}") * utils::intToGrouped(part_top));
+ }
+ else if (params.effective_vis_mode == 1)
+ {
+ divTitle = ext::str(ext::kformat(i18n(muT("[#{day}] #{amount}")))
+ % muT("#{day}") * utils::stripPrefix(muT("wday3:"), i18n(szWDayName[j]))
+ % muT("#{amount}") * utils::intToGrouped(part_top));
+ }
+ else
+ {
+ divTitle = ext::str(ext::kformat(i18n(muT("#{amount}")))
+ % muT("#{amount}") * utils::intToGrouped(part_top));
+ }
+
+ tos << muT("<div title=\"") << utils::htmlEscape(divTitle) << muT("\" style=\"left: ") << (5 * j) << muT("px;\">");
+ }
+ else if (part_top != 0)
+ {
+ tos << muT("<div style=\"left: ") << (5 * j) << muT("px;\">");
+ }
+
+ if (part_top != 0)
+ {
+ int bar_len = (50 * part_top + top - 1) / top;
+
+ tos << muT("<div style=\"top: ") << (50 - bar_len) << muT("px;\"></div>");
+ }
+
+ if (m_bDetail || part_top != 0)
+ {
+ tos << muT("</div>") << ext::endl;
+ }
+ }
+
+ tos << muT("</div></td>") << ext::endl;
+ }
+}
diff --git a/plugins/HistoryStats/src/column_split.h b/plugins/HistoryStats/src/column_split.h
new file mode 100644
index 0000000000..03d0ce6055
--- /dev/null
+++ b/plugins/HistoryStats/src/column_split.h
@@ -0,0 +1,74 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_SPLIT_H)
+#define HISTORYSTATS_GUARD_COLUMN_SPLIT_H
+
+#include "column.h"
+
+/*
+ * ColSplit
+ */
+
+class ColSplit
+ : public Column
+{
+public:
+ struct SplitParams {
+ int alignment; // 0 = day, 1 = week
+ int hours_in_block;
+ int blocks_in_column;
+ int effective_vis_mode;
+ };
+
+private:
+ int m_nSource; // 0 = characters, 1 = messages, 2 = chats
+ int m_nSourceType; // 0 = in, 1 = out, 2 = total
+ int m_nVisMode; // 0 = hours of day, 1 = days of week, 2 = custom
+ bool m_bDetail;
+ int m_nBlockUnit; // 0 = hours, 1 = days, 2 = weeks
+ int m_nUnitsPerBlock;
+ int m_nBlocks;
+ int m_nGraphAlign; // 0 = day boundary, 1 = week boundary
+
+ OptionsCtrl::Combo m_hSource;
+ OptionsCtrl::Radio m_hVisMode;
+ OptionsCtrl::Check m_hDetail;
+ OptionsCtrl::Combo m_hBlockUnit;
+ OptionsCtrl::Edit m_hUnitsPerBlock;
+ OptionsCtrl::Edit m_hBlocks;
+ OptionsCtrl::Radio m_hGraphAlign;
+
+ ext::string m_CSS;
+ DWORD m_nTimeDiv;
+ DWORD m_nTimeMod;
+ DWORD m_nTimeOffset;
+
+private:
+ SplitParams getParams() const;
+ void addToSlot(Contact& contact, DWORD localTimestamp, int toAdd);
+
+public:
+ explicit ColSplit();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColSplit; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("\"Split\"")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding a graphical overview of your chatting amount split by day of week or by hour of day. Different chatting amount measures are available.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig | cfAcquiresData; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const;
+ virtual ext::string impl_contactDataGetUID() const;
+ virtual void impl_contactDataBeginAcquire();
+ virtual void impl_contactDataPrepare(Contact& contact) const;
+ virtual void impl_contactDataFree(Contact& contact) const;
+ virtual void impl_contactDataAcquireMessage(Contact& contact, Message& msg);
+ virtual void impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration);
+ virtual void impl_contactDataMerge(Contact& contact, const Contact& include) const;
+ virtual StyleList impl_outputGetAdditionalStyles(IDProvider& idp);
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_SPLIT_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_splittimeline.cpp b/plugins/HistoryStats/src/column_splittimeline.cpp
new file mode 100644
index 0000000000..f856167d04
--- /dev/null
+++ b/plugins/HistoryStats/src/column_splittimeline.cpp
@@ -0,0 +1,638 @@
+#include "_globals.h"
+#include "column_splittimeline.h"
+
+/*
+ * ColSplitTimeline
+ */
+
+ColSplitTimeline::ColSplitTimeline()
+ : m_nSource(0), m_nSourceType(2), m_nIgnoreOld(0), m_nVisMode(0),
+ m_nHODGroup(1), m_nDOWGroup(1), m_nBlockUnit(0), m_nUnitsPerBlock(6),
+ m_nBlocks(28), m_nGraphAlign(1), m_nCustomGroup(1), m_bTopPerColumn(true),
+ m_hSource(NULL), m_hIgnoreOld(NULL), m_hVisMode(NULL),
+ m_hHODGroup(NULL), m_hDOWGroup(NULL), m_hBlockUnit(NULL), m_hUnitsPerBlock(NULL),
+ m_hBlocks(NULL), m_hGraphAlign(NULL), m_hCustomGroup(NULL),m_hTopPerColumn(NULL),
+ m_nTimeDiv(3600), m_nTimeOffset(0),
+ m_nTimelineWidth(0), m_nBlockOffset(0), m_nNumBlocks(0)
+{
+}
+
+void ColSplitTimeline::impl_copyConfig(const Column* pSource)
+{
+ const ColSplitTimeline& src = *reinterpret_cast<const ColSplitTimeline*>(pSource);
+
+ m_nSource = src.m_nSource;
+ m_nSourceType = src.m_nSourceType;
+ m_nIgnoreOld = src.m_nIgnoreOld;
+ m_nVisMode = src.m_nVisMode;
+ m_nHODGroup = src.m_nHODGroup;
+ m_nDOWGroup = src.m_nDOWGroup;
+ m_nBlockUnit = src.m_nBlockUnit;
+ m_nUnitsPerBlock = src.m_nUnitsPerBlock;
+ m_nBlocks = src.m_nBlocks;
+ m_nGraphAlign = src.m_nGraphAlign;
+ m_nCustomGroup = src.m_nCustomGroup;
+ m_bTopPerColumn = src.m_bTopPerColumn;
+}
+
+void ColSplitTimeline::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource, 0, 0, 2);
+ m_nSourceType = settings.readIntRanged(con::KeySourceType, 2, 0, 3);
+ m_nIgnoreOld = settings.readIntRanged(con::KeyIgnoreOld, 0, 0, 1000);
+ m_nVisMode = settings.readIntRanged(con::KeyVisMode, 0, 0, 2);
+ m_nHODGroup = settings.readIntRanged(con::KeyHODGroup, 1, 1, 1000);
+ m_nDOWGroup = settings.readIntRanged(con::KeyDOWGroup, 1, 1, 1000);
+ m_nBlockUnit = settings.readIntRanged(con::KeyBlockUnit, 0, 0, 2);
+ m_nUnitsPerBlock = settings.readIntRanged(con::KeyUnitsPerBlock, 6, 1, 100);
+ m_nBlocks = settings.readIntRanged(con::KeyBlocks, 28, 1, 49);
+ m_nGraphAlign = settings.readIntRanged(con::KeyGraphAlign, 1, 0, 1);
+ m_nCustomGroup = settings.readIntRanged(con::KeyCustomGroup, 1, 1, 1000);
+ m_bTopPerColumn = settings.readBool(con::KeyTopPerColumn, true);
+}
+
+void ColSplitTimeline::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeySource, m_nSource);
+ settings.writeInt (con::KeySourceType, m_nSourceType);
+ settings.writeInt (con::KeyIgnoreOld, m_nIgnoreOld);
+ settings.writeInt (con::KeyVisMode, m_nVisMode);
+ settings.writeInt (con::KeyHODGroup, m_nHODGroup);
+ settings.writeInt (con::KeyDOWGroup, m_nDOWGroup);
+ settings.writeInt (con::KeyBlockUnit, m_nBlockUnit);
+ settings.writeInt (con::KeyUnitsPerBlock, m_nUnitsPerBlock);
+ settings.writeInt (con::KeyBlocks, m_nBlocks);
+ settings.writeInt (con::KeyGraphAlign, m_nGraphAlign);
+ settings.writeInt (con::KeyCustomGroup, m_nCustomGroup);
+ settings.writeBool(con::KeyTopPerColumn, m_bTopPerColumn);
+}
+
+void ColSplitTimeline::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ OptionsCtrl::Group hTemp;
+ OptionsCtrl::Radio hTempRadio;
+
+ /**/m_hSource = Opt.insertCombo(hGroup, i18n(muT("Data source")));
+ /**/m_hIgnoreOld = Opt.insertEdit(hGroup, i18n(muT("Drop everything older than (days, 0=no limit)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("\"Split\" type")));
+ /**/ m_hVisMode = Opt.insertRadio(hTemp, NULL, i18n(muT("Hours of day")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hHODGroup = Opt.insertEdit (m_hVisMode, i18n(muT("Number of days to group")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ hTempRadio = Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Days of week")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hDOWGroup = Opt.insertEdit (hTempRadio, i18n(muT("Number of weeks to group")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ hTempRadio = Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Custom (for experts only)")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ hTemp = Opt.insertGroup(hTempRadio, i18n(muT("Column setup")));
+ /**/ m_hBlockUnit = Opt.insertCombo(hTemp, i18n(muT("Block unit")));
+ /**/ m_hUnitsPerBlock = Opt.insertEdit (hTemp, i18n(muT("Units per block")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hBlocks = Opt.insertEdit (hTemp, i18n(muT("Blocks per column")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ hTemp = Opt.insertGroup(hTempRadio, i18n(muT("Graph alignment")));
+ /**/ m_hGraphAlign = Opt.insertRadio(hTemp, NULL, i18n(muT("Align on day boundary")));
+ /**/ Opt.insertRadio(hTemp, m_hGraphAlign, i18n(muT("Align on week boundary")));
+ /**/ m_hCustomGroup = Opt.insertEdit (hTempRadio, i18n(muT("Number of columns to group")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/m_hTopPerColumn = Opt.insertCheck(hGroup, i18n(muT("Calculate maximum per column (not per graph)")));
+
+ static const mu_text* sourceTexts[] = {
+ I18N(muT("Characters (incoming)")),
+ I18N(muT("Characters (outgoing)")),
+ I18N(muT("Characters (all)")),
+ I18N(muT("Characters (in/out ratio)")),
+ I18N(muT("Messages (incoming)")),
+ I18N(muT("Messages (outgoing)")),
+ I18N(muT("Messages (all)")),
+ I18N(muT("Messages (in/out ratio)")),
+ I18N(muT("Chats (incoming)")),
+ I18N(muT("Chats (outgoing)")),
+ I18N(muT("Chats (all)")),
+ I18N(muT("Chats (in/out ratio)")),
+ };
+
+ array_each_(i, sourceTexts)
+ {
+ Opt.addComboItem(m_hSource, i18n(sourceTexts[i]));
+ }
+
+ static const mu_text* unitTexts[] = {
+ I18N(muT("Hours")),
+ I18N(muT("Days")),
+ I18N(muT("Weeks")),
+ };
+
+ array_each_(i, unitTexts)
+ {
+ Opt.addComboItem(m_hBlockUnit, i18n(unitTexts[i]));
+ }
+
+ Opt.setComboSelected(m_hSource , 4 * m_nSource + m_nSourceType);
+ Opt.setEditNumber (m_hIgnoreOld , m_nIgnoreOld );
+ Opt.setRadioChecked (m_hVisMode , m_nVisMode );
+ Opt.setEditNumber (m_hHODGroup , m_nHODGroup );
+ Opt.setEditNumber (m_hDOWGroup , m_nDOWGroup );
+ Opt.setComboSelected(m_hBlockUnit , m_nBlockUnit );
+ Opt.setEditNumber (m_hUnitsPerBlock, m_nUnitsPerBlock );
+ Opt.setEditNumber (m_hBlocks , m_nBlocks );
+ Opt.setRadioChecked (m_hGraphAlign , m_nGraphAlign );
+ Opt.setEditNumber (m_hCustomGroup , m_nCustomGroup );
+ Opt.checkItem (m_hTopPerColumn , m_bTopPerColumn );
+}
+
+void ColSplitTimeline::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getComboSelected(m_hSource ) / 4;
+ m_nSourceType = Opt.getComboSelected(m_hSource ) % 4;
+ m_nIgnoreOld = Opt.getEditNumber (m_hIgnoreOld );
+ m_nVisMode = Opt.getRadioChecked (m_hVisMode );
+ m_nHODGroup = Opt.getEditNumber (m_hHODGroup );
+ m_nDOWGroup = Opt.getEditNumber (m_hDOWGroup );
+ m_nBlockUnit = Opt.getComboSelected(m_hBlockUnit );
+ m_nUnitsPerBlock = Opt.getEditNumber (m_hUnitsPerBlock);
+ m_nBlocks = Opt.getEditNumber (m_hBlocks );
+ m_nGraphAlign = Opt.getRadioChecked (m_hGraphAlign );
+ m_nCustomGroup = Opt.getEditNumber (m_hCustomGroup );
+ m_bTopPerColumn = Opt.isItemChecked (m_hTopPerColumn );
+
+ // ensure constraints
+ utils::ensureRange(m_nIgnoreOld, 0, 1000, 0);
+ utils::ensureRange(m_nHODGroup, 1, 1000, 1);
+ utils::ensureRange(m_nDOWGroup, 1, 1000, 1);
+ utils::ensureRange(m_nUnitsPerBlock, 1, 100, 6);
+ utils::ensureRange(m_nBlocks, 1, 49, 28);
+ utils::ensureRange(m_nCustomGroup, 1, 1000, 1);
+}
+
+ext::string ColSplitTimeline::impl_contactDataGetUID() const
+{
+ SplitParams params = getParams();
+
+ return ext::str(ext::format(muT("splittimeline-|-|-|"))
+ % m_nSource
+ % params.hours_in_block
+ % params.alignment);
+}
+
+void ColSplitTimeline::impl_contactDataBeginAcquire()
+{
+ SplitParams params = getParams();
+
+ m_nTimeDiv = 3600 * params.hours_in_block;
+
+ if (params.alignment == 1)
+ {
+ DWORD dwOffset = 0;
+ tm offsetTM = *gmtime(reinterpret_cast<const time_t*>(&dwOffset));
+
+ m_nTimeOffset = 86400 * ((offsetTM.tm_wday + 6) % 7);
+ }
+ else
+ {
+ m_nTimeOffset = 0;
+ }
+}
+
+void ColSplitTimeline::impl_contactDataPrepare(Contact& contact) const
+{
+ TimelineMap* pData = new TimelineMap;
+
+ contact.setSlot(contactDataSlotGet(), pData);
+}
+
+void ColSplitTimeline::impl_contactDataFree(Contact& contact) const
+{
+ TimelineMap* pData = reinterpret_cast<TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ if (pData)
+ {
+ delete pData;
+ contact.setSlot(contactDataSlotGet(), NULL);
+ }
+}
+
+void ColSplitTimeline::addToSlot(Contact& contact, bool bOutgoing, DWORD localTimestamp, int toAdd)
+{
+ if (toAdd > 0)
+ {
+ TimelineMap* pData = reinterpret_cast<TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ InOut& io = (*pData)[(localTimestamp + m_nTimeOffset) / m_nTimeDiv];
+
+ (bOutgoing ? io.out : io.in) += toAdd;
+ }
+}
+
+void ColSplitTimeline::impl_contactDataAcquireMessage(Contact& contact, Message& msg)
+{
+ if (m_nSource == 0)
+ {
+ addToSlot(contact, msg.isOutgoing(), msg.getTimestamp(), msg.getLength());
+ }
+ else if (m_nSource == 1)
+ {
+ addToSlot(contact, msg.isOutgoing(), msg.getTimestamp(), 1);
+ }
+}
+
+void ColSplitTimeline::impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration)
+{
+ if (m_nSource == 2)
+ {
+ addToSlot(contact, bOutgoing, localTimestampStarted, 1);
+ }
+}
+
+void ColSplitTimeline::impl_contactDataMerge(Contact& contact, const Contact& include) const
+{
+ TimelineMap* pData = reinterpret_cast<TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+ const TimelineMap* pIncData = reinterpret_cast<const TimelineMap*>(include.getSlot(contactDataSlotGet()));
+
+ citer_each_(TimelineMap, i, *pIncData)
+ {
+ (*pData)[i->first] += i->second;
+ }
+}
+
+void ColSplitTimeline::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szTypeDesc[] = {
+ I18N(muT("Hours of day timeline")),
+ I18N(muT("Days of week timeline")),
+ I18N(muT("\"Split\" timeline")),
+ };
+
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("incoming characters")),
+ I18N(muT("outgoing characters")),
+ I18N(muT("all characters")),
+ I18N(muT("in/out ratio of characters")),
+ I18N(muT("incoming messages")),
+ I18N(muT("outgoing messages")),
+ I18N(muT("all messages")),
+ I18N(muT("in/out ratio of messages")),
+ I18N(muT("incoming chats")),
+ I18N(muT("outgoing chats")),
+ I18N(muT("all chats")),
+ I18N(muT("in/out ratio of chats")),
+ };
+
+ if (row == 1)
+ {
+ SplitParams params = getParams();
+ ext::string strTitle = str(ext::kformat(i18n(muT("#{type} for #{data}")))
+ % muT("#{type}") * i18n(szTypeDesc[params.effective_vis_mode])
+ % muT("#{data}") * i18n(szSourceDesc[4 * m_nSource + m_nSourceType]));
+
+ writeRowspanTD(tos, getCustomTitle(i18n(szTypeDesc[params.effective_vis_mode]), strTitle) + muT("<div style=\"width: ") + utils::intToString(m_nTimelineWidth) + muT("px;\"></div>"), row, 1, rowSpan);
+ }
+}
+
+void ColSplitTimeline::impl_columnDataAfterOmit()
+{
+ // AFTER, i.e. contacts are trimmed to what user will see
+
+ // init columns that are active but not acquireing
+ impl_contactDataBeginAcquire();
+
+ // put _all_ available contacts (including omitted/total) in one list
+ Statistic::ContactListC l;
+
+ upto_each_(i, getStatistic()->countContacts())
+ {
+ l.push_back(&getStatistic()->getContact(i));
+ }
+
+ if (getStatistic()->hasOmitted())
+ {
+ l.push_back(&getStatistic()->getOmitted());
+ }
+
+ if (getStatistic()->hasTotals())
+ {
+ l.push_back(&getStatistic()->getTotals());
+ }
+
+ SplitParams params = getParams();
+
+ if (l.size() > 0)
+ {
+ DWORD nFirstTime = con::MaxDateTime, nLastTime = con::MinDateTime;
+
+ citer_each_(Statistic::ContactListC, c, l)
+ {
+ if ((*c)->isFirstLastTimeValid())
+ {
+ nFirstTime = min(nFirstTime, (*c)->getFirstTime());
+ nLastTime = max(nLastTime, (*c)->getLastTime());
+ }
+ }
+
+ if (nFirstTime == con::MaxDateTime && nLastTime == con::MinDateTime)
+ {
+ nFirstTime = nLastTime = 0;
+ }
+
+ // honour ignore setting
+ if (m_nIgnoreOld > 0 && nLastTime > m_nIgnoreOld * 86400 && nLastTime - nFirstTime > m_nIgnoreOld * 86400)
+ {
+ nFirstTime = nLastTime - m_nIgnoreOld * 86400;
+ }
+
+ // always align to day boundary
+ nFirstTime = (nFirstTime / 86400) * 86400;
+
+ if (params.alignment == 1)
+ {
+ // align on week boundary
+ nFirstTime = ((nFirstTime + m_nTimeOffset) / 604800) * 604800 - m_nTimeOffset;
+ }
+
+ // correct with "time offset"
+ nFirstTime += m_nTimeOffset;
+ nLastTime += m_nTimeOffset;
+
+ m_nBlockOffset = nFirstTime / m_nTimeDiv;
+ m_nNumBlocks = nLastTime / m_nTimeDiv - m_nBlockOffset + 1;
+ }
+ else
+ {
+ m_nBlockOffset = m_nNumBlocks = 0;
+ }
+
+ m_nTimelineWidth = 3 * ((m_nNumBlocks + params.blocks_in_column * params.columns_to_group - 1) / (params.blocks_in_column * params.columns_to_group));
+}
+
+void ColSplitTimeline::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ if (m_nSourceType != 3)
+ {
+ outputRenderRowInOut(tos, contact, display);
+ }
+ else
+ {
+ outputRenderRowRatio(tos, contact, display);
+ }
+}
+
+ColSplitTimeline::SplitParams ColSplitTimeline::getParams() const
+{
+ static const int unitFactors[] = { 1, 24, 168 };
+
+ SplitParams params;
+
+ params.effective_vis_mode = m_nVisMode;
+
+ switch (m_nVisMode)
+ {
+ case 0: // hours of day
+ {
+ params.alignment = 0;
+ params.columns_to_group = m_nHODGroup;
+ params.hours_in_block = 1;
+ params.blocks_in_column = 24;
+ }
+ break;
+
+ case 1: // days of week
+ {
+ params.alignment = 1;
+ params.columns_to_group = m_nDOWGroup;
+ params.hours_in_block = 24;
+ params.blocks_in_column = 7;
+ }
+ break;
+
+ case 2: // custom
+ {
+ params.alignment = m_nGraphAlign;
+ params.columns_to_group = m_nCustomGroup;
+ params.hours_in_block = unitFactors[m_nBlockUnit] * m_nUnitsPerBlock;
+ params.blocks_in_column = m_nBlocks;
+
+ // heuristics for custom mode
+ if (params.hours_in_block == 1 && params.blocks_in_column == 24)
+ {
+ params.effective_vis_mode = 0;
+ }
+ else if (params.hours_in_block == 24 && params.blocks_in_column == 7 && params.alignment == 1)
+ {
+ params.effective_vis_mode = 1;
+ }
+ }
+ break;
+ }
+
+ return params;
+}
+
+void ColSplitTimeline::outputRenderRowInOut(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ SplitParams params = getParams();
+ const TimelineMap* pData = reinterpret_cast<const TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ int top = 0;
+
+ if (!m_bTopPerColumn)
+ {
+ // calculate a global maximum
+ for (int curBlock = 0; curBlock < m_nNumBlocks; curBlock += params.blocks_in_column * params.columns_to_group)
+ {
+ for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock)
+ {
+ int part_top = 0;
+ DWORD block_time = m_nBlockOffset + curBlock + partBlock;
+
+ for (int curCol = 0; curCol < params.columns_to_group; ++curCol)
+ {
+ DWORD cur_time = block_time + curCol * params.blocks_in_column;
+
+ TimelineMap::const_iterator i = pData->find(cur_time);
+
+ if (i != pData->end())
+ {
+ part_top += getValue(i->second);
+ }
+ }
+
+ top = max(top, part_top);
+ }
+ }
+
+ if (top == 0)
+ {
+ top = 1;
+ }
+ }
+
+ tos << muT("<td class=\"img_middle\">");
+
+ // draw graph
+ Canvas canvas(m_nTimelineWidth, 49);
+
+ canvas.fillBackground(con::ColorBack);
+
+ COLORREF colorTab[256];
+
+ utils::generateGradient(con::ColorBack, con::ColorBar, colorTab);
+
+ HDC hDC = canvas.beginDraw();
+
+ for (int curBlock = 0; curBlock < m_nNumBlocks; curBlock += params.blocks_in_column * params.columns_to_group)
+ {
+ int from_left = 3 * curBlock / (params.blocks_in_column * params.columns_to_group);
+
+ if (m_bTopPerColumn)
+ {
+ // calculate a local maximum (per column)
+ top = 0;
+
+ for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock)
+ {
+ int part_top = 0;
+ DWORD block_time = m_nBlockOffset + curBlock + partBlock;
+
+ for (int curCol = 0; curCol < params.columns_to_group; ++curCol)
+ {
+ DWORD cur_time = block_time + curCol * params.blocks_in_column;
+
+ TimelineMap::const_iterator i = pData->find(cur_time);
+
+ if (i != pData->end())
+ {
+ part_top += getValue(i->second);
+ }
+ }
+
+ top = max(top, part_top);
+ }
+
+ if (top == 0)
+ {
+ top = 1;
+ }
+ }
+
+ for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock)
+ {
+ int part_top = 0;
+ DWORD block_time = m_nBlockOffset + curBlock + partBlock;
+
+ for (int curCol = 0; curCol < params.columns_to_group; ++curCol)
+ {
+ DWORD cur_time = block_time + curCol * params.blocks_in_column;
+
+ TimelineMap::const_iterator i = pData->find(cur_time);
+
+ if (i != pData->end())
+ {
+ part_top += getValue(i->second);
+ }
+ }
+
+ if (part_top != 0)
+ {
+ RECT r = {
+ from_left,
+ 49 * partBlock / params.blocks_in_column,
+ from_left + 3,
+ 49 * (partBlock + 1) / params.blocks_in_column
+ };
+
+ int color = 255 * part_top / top;
+
+ SetBkColor(hDC, colorTab[color]);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &r, NULL, 0, NULL);
+ }
+ }
+ }
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<img src=\"") << strFinalFile << muT("\"/>");
+ }
+
+ tos << muT("</td>") << ext::endl;
+}
+
+void ColSplitTimeline::outputRenderRowRatio(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ SplitParams params = getParams();
+ const TimelineMap* pData = reinterpret_cast<const TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ tos << muT("<td class=\"img_middle\">");
+
+ // draw graph
+ Canvas canvas(m_nTimelineWidth, 49);
+
+ canvas.fillBackground(con::ColorBack);
+
+ COLORREF inColorTab[256], outColorTab[256];
+
+ utils::generateGradient(con::ColorBack, con::ColorIn, inColorTab);
+ utils::generateGradient(con::ColorBack, con::ColorOut, outColorTab);
+
+ HDC hDC = canvas.beginDraw();
+
+ for (int curBlock = 0; curBlock < m_nNumBlocks; curBlock += params.blocks_in_column * params.columns_to_group)
+ {
+ int from_left = 3 * curBlock / (params.blocks_in_column * params.columns_to_group);
+
+ for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock)
+ {
+ int part_in = 0;
+ int part_out = 0;
+ DWORD block_time = m_nBlockOffset + curBlock + partBlock;
+
+ for (int curCol = 0; curCol < params.columns_to_group; ++curCol)
+ {
+ DWORD cur_time = block_time + curCol * params.blocks_in_column;
+
+ TimelineMap::const_iterator i = pData->find(cur_time);
+
+ if (i != pData->end())
+ {
+ part_in += i->second.in;
+ part_out += i->second.out;
+ }
+ }
+
+ int part_sum = part_in + part_out;
+
+ if (part_sum != 0)
+ {
+ RECT r = {
+ from_left,
+ 49 * partBlock / params.blocks_in_column,
+ from_left + 3,
+ 49 * (partBlock + 1) / params.blocks_in_column
+ };
+
+ COLORREF color = inColorTab[0];
+
+ if (part_in > part_out)
+ {
+ color = inColorTab[255 * (2 * part_in - part_sum) / part_sum];
+ }
+ else if (part_in < part_out)
+ {
+ color = outColorTab[255 * (2 * part_out - part_sum) / part_sum];
+ }
+
+ SetBkColor(hDC, color);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &r, NULL, 0, NULL);
+ }
+ }
+ }
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<img src=\"") << strFinalFile << muT("\"/>");
+ }
+
+ tos << muT("</td>") << ext::endl;
+}
diff --git a/plugins/HistoryStats/src/column_splittimeline.h b/plugins/HistoryStats/src/column_splittimeline.h
new file mode 100644
index 0000000000..e3754f5891
--- /dev/null
+++ b/plugins/HistoryStats/src/column_splittimeline.h
@@ -0,0 +1,101 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_SPLITTIMELINE_H)
+#define HISTORYSTATS_GUARD_COLUMN_SPLITTIMELINE_H
+
+#include "column.h"
+
+/*
+ * ColSplitTimeline
+ */
+
+class ColSplitTimeline
+ : public Column
+{
+public:
+ typedef std::map<int, InOut> TimelineMap;
+
+ struct SplitParams {
+ int alignment; // 0 = day, 1 = week
+ int columns_to_group;
+ int hours_in_block;
+ int blocks_in_column;
+ int effective_vis_mode;
+ };
+
+private:
+ int m_nSource; // 0 = characters, 1 = messages, 2 = chats
+ int m_nSourceType; // 0 = in, 1 = out, 2 = total, 3 = ratio
+ int m_nIgnoreOld;
+ int m_nVisMode; // 0 = hours of day, 1 = days of week, 2 = custom
+ int m_nHODGroup;
+ int m_nDOWGroup;
+ int m_nBlockUnit; // 0 = hours, 1 = days, 2 = weeks
+ int m_nUnitsPerBlock;
+ int m_nBlocks;
+ int m_nGraphAlign; // 0 = day boundary, 1 = week boundary
+ int m_nCustomGroup;
+ bool m_bTopPerColumn;
+
+ OptionsCtrl::Combo m_hSource;
+ OptionsCtrl::Edit m_hIgnoreOld;
+ OptionsCtrl::Radio m_hVisMode;
+ OptionsCtrl::Edit m_hHODGroup;
+ OptionsCtrl::Edit m_hDOWGroup;
+ OptionsCtrl::Combo m_hBlockUnit;
+ OptionsCtrl::Edit m_hUnitsPerBlock;
+ OptionsCtrl::Edit m_hBlocks;
+ OptionsCtrl::Radio m_hGraphAlign;
+ OptionsCtrl::Edit m_hCustomGroup;
+ OptionsCtrl::Check m_hTopPerColumn;
+
+ DWORD m_nTimeDiv;
+ DWORD m_nTimeOffset;
+ int m_nTimelineWidth;
+ int m_nBlockOffset;
+ int m_nNumBlocks;
+
+private:
+ int getValue(const InOut& value)
+ {
+ switch (m_nSourceType)
+ {
+ case 0: return value.in;
+ case 1: return value.out;
+ case 2: return value.total();
+ }
+
+ return 0;
+ }
+
+ SplitParams getParams() const;
+ void addToSlot(Contact& contact, bool bOutgoing, DWORD localTimestamp, int toAdd);
+
+ void outputRenderRowInOut(ext::ostream& tos, const Contact& contact, DisplayType display);
+ void outputRenderRowRatio(ext::ostream& tos, const Contact& contact, DisplayType display);
+
+public:
+ explicit ColSplitTimeline();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColSplitTimeline; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("\"Split\" timeline")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding a graphical overview of your chatting behaviour (out, in, total, in/out ratio) from the first to the last day of your history. The information is spread along x- and y-axis and the values are encoded as color values. Different chatting behaviour measures are available.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig | cfAcquiresData; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const { return crPNGFull; }
+ virtual ext::string impl_contactDataGetUID() const;
+ virtual void impl_contactDataBeginAcquire();
+ virtual void impl_contactDataPrepare(Contact& contact) const;
+ virtual void impl_contactDataFree(Contact& contact) const;
+ virtual void impl_contactDataAcquireMessage(Contact& contact, Message& msg);
+ virtual void impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration);
+ virtual void impl_contactDataMerge(Contact& contact, const Contact& include) const;
+ virtual void impl_columnDataAfterOmit();
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_SPLITTIMELINE_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_timeline.cpp b/plugins/HistoryStats/src/column_timeline.cpp
new file mode 100644
index 0000000000..0caeb6564a
--- /dev/null
+++ b/plugins/HistoryStats/src/column_timeline.cpp
@@ -0,0 +1,536 @@
+#include "_globals.h"
+#include "column_timeline.h"
+
+/*
+ * ColTimeline
+ */
+
+ColTimeline::ColTimeline()
+ : m_nSource(0), m_nSourceType(2), m_nIgnoreOld(0), m_bDetail(true), m_nDays(7),
+ m_hSource(NULL), m_hIgnoreOld(NULL), m_hDetail(NULL), m_hDays(NULL)
+{
+}
+
+void ColTimeline::impl_copyConfig(const Column* pSource)
+{
+ const ColTimeline& src = *reinterpret_cast<const ColTimeline*>(pSource);
+
+ m_nSource = src.m_nSource;
+ m_nSourceType = src.m_nSourceType;
+ m_nIgnoreOld = src.m_nIgnoreOld;
+ m_bDetail = src.m_bDetail;
+ m_nDays = src.m_nDays;
+}
+
+void ColTimeline::impl_configRead(const SettingsTree& settings)
+{
+ m_nSource = settings.readIntRanged(con::KeySource , 0, 0, 2);
+ m_nSourceType = settings.readIntRanged(con::KeySourceType, 2, 0, 3);
+ m_nIgnoreOld = settings.readIntRanged(con::KeyIgnoreOld , 0, 0, 1000);
+ m_bDetail = settings.readBool (con::KeyDetail , true);
+ m_nDays = settings.readIntRanged(con::KeyDays , 7, 1, 1000);
+}
+
+void ColTimeline::impl_configWrite(SettingsTree& settings) const
+{
+ settings.writeInt (con::KeySource , m_nSource );
+ settings.writeInt (con::KeySourceType, m_nSourceType);
+ settings.writeInt (con::KeyIgnoreOld , m_nIgnoreOld );
+ settings.writeBool(con::KeyDetail , m_bDetail );
+ settings.writeInt (con::KeyDays , m_nDays );
+}
+
+void ColTimeline::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ /**/m_hSource = Opt.insertCombo(hGroup, i18n(muT("Data source")));
+ /**/m_hIgnoreOld = Opt.insertEdit(hGroup, i18n(muT("Drop everything older than (days, 0=no limit)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("Details for every bar (tooltip)")));
+ /**/m_hDays = Opt.insertEdit (hGroup, i18n(muT("Number of days to group")), muT(""), OptionsCtrl::OCF_NUMBER);
+
+ static const mu_text* sourceTexts[] = {
+ I18N(muT("Characters (incoming)")),
+ I18N(muT("Characters (outgoing)")),
+ I18N(muT("Characters (all)")),
+ I18N(muT("Characters (in/out ratio)")),
+ I18N(muT("Messages (incoming)")),
+ I18N(muT("Messages (outgoing)")),
+ I18N(muT("Messages (all)")),
+ I18N(muT("Messages (in/out ratio)")),
+ I18N(muT("Chats (incoming)")),
+ I18N(muT("Chats (outgoing)")),
+ I18N(muT("Chats (all)")),
+ I18N(muT("Chats (in/out ratio)")),
+ };
+
+ array_each_(i, sourceTexts)
+ {
+ Opt.addComboItem(m_hSource, i18n(sourceTexts[i]));
+ }
+
+ Opt.setComboSelected(m_hSource , 4 * m_nSource + m_nSourceType);
+ Opt.setEditNumber (m_hIgnoreOld, m_nIgnoreOld );
+ Opt.checkItem (m_hDetail , m_bDetail );
+ Opt.setEditNumber (m_hDays , m_nDays );
+}
+
+void ColTimeline::impl_configFromUI(OptionsCtrl& Opt)
+{
+ m_nSource = Opt.getComboSelected(m_hSource) / 4;
+ m_nSourceType = Opt.getComboSelected(m_hSource) % 4;
+ m_nIgnoreOld = Opt.getEditNumber (m_hIgnoreOld);
+ m_bDetail = Opt.isItemChecked (m_hDetail);
+ m_nDays = Opt.getEditNumber (m_hDays);
+
+ // ensure constraints
+ utils::ensureRange(m_nIgnoreOld, 0, 1000, 0);
+}
+
+int ColTimeline::impl_configGetRestrictions(ext::string* pDetails) const
+{
+ if (pDetails && m_bDetail)
+ {
+ *pDetails = i18n(muT("Details for every bar (tooltip) are only available with HTML output."));
+ }
+
+ // m_bDetail "on" means we need tooltips and they are not available with PNG output
+ return crHTMLFull | (m_bDetail ? crPNGPartial : crPNGFull);
+}
+
+ext::string ColTimeline::impl_contactDataGetUID() const
+{
+ return ext::str(ext::format(muT("timeline-|")) % m_nSource);
+}
+
+void ColTimeline::impl_contactDataPrepare(Contact& contact) const
+{
+ TimelineMap* pData = new TimelineMap;
+
+ contact.setSlot(contactDataSlotGet(), pData);
+}
+
+void ColTimeline::impl_contactDataFree(Contact& contact) const
+{
+ TimelineMap* pData = reinterpret_cast<TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ if (pData)
+ {
+ delete pData;
+ contact.setSlot(contactDataSlotGet(), NULL);
+ }
+}
+
+void ColTimeline::addToSlot(Contact& contact, bool bOutgoing, DWORD localTimestamp, int toAdd)
+{
+ if (toAdd > 0)
+ {
+ TimelineMap* pData = reinterpret_cast<TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ InOut& io = (*pData)[localTimestamp / 86400];
+
+ (bOutgoing ? io.out : io.in) += toAdd;
+ }
+}
+
+void ColTimeline::impl_contactDataAcquireMessage(Contact& contact, Message& msg)
+{
+ if (m_nSource == 0)
+ {
+ addToSlot(contact, msg.isOutgoing(), msg.getTimestamp(), msg.getLength());
+ }
+ else if (m_nSource == 1)
+ {
+ addToSlot(contact, msg.isOutgoing(), msg.getTimestamp(), 1);
+ }
+}
+
+void ColTimeline::impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration)
+{
+ if (m_nSource == 2)
+ {
+ addToSlot(contact, bOutgoing, localTimestampStarted, 1);
+ }
+}
+
+void ColTimeline::impl_contactDataMerge(Contact& contact, const Contact& include) const
+{
+ TimelineMap* pData = reinterpret_cast<TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+ const TimelineMap* pIncData = reinterpret_cast<const TimelineMap*>(include.getSlot(contactDataSlotGet()));
+
+ citer_each_(TimelineMap, i, *pIncData)
+ {
+ (*pData)[i->first] += i->second;
+ }
+}
+
+Column::StyleList ColTimeline::impl_outputGetAdditionalStyles(IDProvider& idp)
+{
+ StyleList l;
+
+ if (!usePNG())
+ {
+ m_CSS = idp.getID();
+
+ l.push_back(StylePair(muT("div.") + m_CSS, muT("position: relative; left: 50%; margin-left: -") + utils::intToString(m_nTimelineWidth / 2) + muT("px; width: ") + utils::intToString(m_nTimelineWidth) + muT("px; height: 49px;")));
+
+ if (m_nSourceType != 3)
+ {
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div"), muT("position: absolute; top: 0px; width: 3px; height: 49px; overflow: hidden;")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div div"), muT("position: absolute; left: 0px; width: 3px; background-color: ") + utils::colorToHTML(con::ColorBar) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div.l"), muT("position: absolute; top: 24px; left: 0px; height: 1px; width: ") + utils::intToString(m_nTimelineWidth) + muT("px; background-color: ") + utils::colorToHTML(con::ColorBarLine) + muT("; z-index: 9;")));
+ }
+ else
+ {
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div"), muT("position: absolute; top: 0px; width: 3px; height: 49px; overflow: hidden; z-index: 9;")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div div.o"), muT("position: absolute; left: 0px; width: 3px; background-color: ") + utils::colorToHTML(con::ColorOut) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div div.i"), muT("position: absolute; top: 24px; left: 0px; width: 3px; background-color: ") + utils::colorToHTML(con::ColorIn) + muT(";")));
+ l.push_back(StylePair(muT("div.") + m_CSS + muT(" div.l"), muT("position: absolute; top: 24px; left: 0px; height: 1px; width: ") + utils::intToString(m_nTimelineWidth) + muT("px; background-color: ") + utils::colorToHTML(con::ColorIOLine) + muT("; z-index: 8;")));
+ }
+ }
+
+ return l;
+}
+
+void ColTimeline::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("incoming characters")),
+ I18N(muT("outgoing characters")),
+ I18N(muT("all characters")),
+ I18N(muT("in/out ratio of characters")),
+ I18N(muT("incoming messages")),
+ I18N(muT("outgoing messages")),
+ I18N(muT("all messages")),
+ I18N(muT("in/out ratio of messages")),
+ I18N(muT("incoming chats")),
+ I18N(muT("outgoing chats")),
+ I18N(muT("all chats")),
+ I18N(muT("in/out ratio of chats")),
+ };
+
+ if (row == 1)
+ {
+ ext::string strTitle = str(ext::kformat(i18n(muT("Timeline for #{data}")))
+ % muT("#{data}") * i18n(szSourceDesc[4 * m_nSource + m_nSourceType]));
+
+ writeRowspanTD(tos, getCustomTitle(i18n(muT("Timeline")), strTitle) + muT("<div style=\"width: ") + utils::intToString(m_nTimelineWidth) + muT("px;\"></div>"), row, 1, rowSpan);
+ }
+}
+
+void ColTimeline::impl_columnDataAfterOmit()
+{
+ // AFTER, i.e. contacts are trimmed to what user will see
+
+ m_nFirstDay = getStatistic()->getFirstTime() / 86400;
+ m_nLastDay = getStatistic()->getLastTime() / 86400;
+
+ // honour ignore setting
+ if (m_nIgnoreOld > 0 && m_nLastDay > m_nIgnoreOld && m_nLastDay - m_nFirstDay > m_nIgnoreOld)
+ {
+ m_nFirstDay = m_nLastDay - m_nIgnoreOld + 1;
+ }
+
+ m_nTimelineWidth = 3 * (1 + (m_nLastDay - m_nFirstDay) / m_nDays);
+}
+
+void ColTimeline::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ if (m_nSourceType != 3)
+ {
+ outputRenderRowInOut(tos, contact, display);
+ }
+ else
+ {
+ outputRenderRowRatio(tos, contact, display);
+ }
+}
+
+void ColTimeline::outputRenderRowInOut(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ const TimelineMap* pData = reinterpret_cast<const TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ int top = 0;
+ int curDay, partDay, part_top;
+
+ for (curDay = m_nFirstDay; curDay <= m_nLastDay; curDay += m_nDays)
+ {
+ for (part_top = 0, partDay = 0; partDay < m_nDays; partDay++)
+ {
+ TimelineMap::const_iterator i = pData->find(curDay + partDay);
+
+ if (i != pData->end())
+ {
+ part_top += getValue(i->second);
+ }
+ }
+
+ top = max(top, part_top);
+ }
+
+ if (top == 0)
+ {
+ top = 1;
+ }
+
+ if (usePNG())
+ {
+ tos << muT("<td class=\"img_middle\">");
+
+ // draw graph
+ Canvas canvas(m_nTimelineWidth, 49);
+
+ canvas.setTrans(con::ColorBack, true);
+
+ HDC hDC = canvas.beginDraw();
+
+ SetBkColor(hDC, con::ColorBar);
+
+ for (curDay = m_nFirstDay; curDay <= m_nLastDay; curDay += m_nDays)
+ {
+ for (part_top = 0, partDay = 0; partDay < m_nDays; partDay++)
+ {
+ TimelineMap::const_iterator i = pData->find(curDay + partDay);
+
+ if (i != pData->end())
+ {
+ part_top += getValue(i->second);
+ }
+ }
+
+ int bar_len = (24 * part_top + top - 1) / top;
+ int from_left = 3 * ((curDay - m_nFirstDay) / m_nDays);
+
+ if (bar_len != 0)
+ {
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(from_left, 24 - bar_len, from_left + 3, 25 + bar_len), NULL, 0, NULL);
+ }
+ }
+
+ SetBkColor(hDC, con::ColorBarLine);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(0, 24, m_nTimelineWidth, 25), NULL, 0, NULL);
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<img src=\"") << strFinalFile << muT("\"/>");
+ }
+
+ tos << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td class=\"bars_middle\">")
+ << muT("<div class=\"") << m_CSS << muT("\">")
+ << muT("<div class=\"l\"></div>") << ext::endl;
+
+ for (curDay = m_nFirstDay; curDay <= m_nLastDay; curDay += m_nDays)
+ {
+ part_top = 0;
+
+ for (partDay = 0; partDay < m_nDays; partDay++)
+ {
+ TimelineMap::const_iterator i = pData->find(curDay + partDay);
+
+ if (i != pData->end())
+ {
+ part_top += getValue(i->second);
+ }
+ }
+
+ int bar_len = (24 * part_top + top - 1) / top;
+
+ if (m_bDetail)
+ {
+ DWORD rightDay = min(curDay + m_nDays - 1, m_nLastDay);
+
+ tos << muT("<div title=\"");
+
+ if (rightDay != curDay)
+ {
+ tos << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[#{start_date}-#{end_date}] #{amount}")))
+ % muT("#{start_date}") * utils::timestampToDate(curDay * 86400)
+ % muT("#{end_date}") * utils::timestampToDate(rightDay * 86400)
+ % muT("#{amount}") * utils::intToGrouped(part_top)));
+ }
+ else
+ {
+ tos << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[#{date}] #{amount}")))
+ % muT("#{date}") * utils::timestampToDate(curDay * 86400)
+ % muT("#{amount}") * utils::intToGrouped(part_top)));
+ }
+
+ tos << muT("\" style=\"left: ")
+ << (3 * ((curDay - m_nFirstDay) / m_nDays)) << muT("px;\">");
+ }
+ else if (bar_len != 0)
+ {
+ tos << muT("<div style=\"left: ") << (3 * ((curDay - m_nFirstDay) / m_nDays)) << muT("px;\">");
+ }
+
+ if (bar_len != 0)
+ {
+ tos << muT("<div style=\"top: ") << (24 - bar_len) << muT("px; height: ") << (1 + 2 * bar_len) << muT("px;\"></div>");
+ }
+
+ if (m_bDetail || bar_len != 0)
+ {
+ tos << muT("</div>") << ext::endl;
+ }
+ }
+
+ tos << muT("</div></td>") << ext::endl;
+ }
+}
+
+void ColTimeline::outputRenderRowRatio(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ const TimelineMap* pData = reinterpret_cast<const TimelineMap*>(contact.getSlot(contactDataSlotGet()));
+
+ int curDay, partDay, part_out, part_in;
+
+ if (usePNG())
+ {
+ tos << muT("<td class=\"img_middle\">");
+
+ // draw graph
+ Canvas canvas(m_nTimelineWidth, 49);
+
+ canvas.setTrans(con::ColorBack, true);
+
+ HDC hDC = canvas.beginDraw();
+
+ SetBkColor(hDC, con::ColorIOLine);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(0, 24, m_nTimelineWidth, 25), NULL, 0, NULL);
+
+ for (curDay = m_nFirstDay; curDay <= m_nLastDay; curDay += m_nDays)
+ {
+ part_out = part_in = 0;
+
+ for (partDay = 0; partDay < m_nDays; partDay++)
+ {
+ TimelineMap::const_iterator i = pData->find(curDay + partDay);
+
+ if (i != pData->end())
+ {
+ part_out += i->second.out;
+ part_in += i->second.in;
+ }
+ }
+
+ int part_sum = part_in + part_out;
+ int bar_len = 0;
+
+ if (part_sum > 0)
+ {
+ bar_len = -24 + 48 * part_out / part_sum;
+ bar_len += (part_out > part_in) ? 1 : 0;
+ }
+
+ int from_left = 3 * ((curDay - m_nFirstDay) / m_nDays);
+
+ if (bar_len < 0)
+ {
+ SetBkColor(hDC, con::ColorIn);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(from_left, 24, from_left + 3, 24 - bar_len), NULL, 0, NULL);
+ }
+ else if (bar_len > 0)
+ {
+ SetBkColor(hDC, con::ColorOut);
+ ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &utils::rect(from_left, 25 - bar_len, from_left + 3, 25), NULL, 0, NULL);
+ }
+ }
+
+ canvas.endDraw();
+
+ // write PNG file
+ ext::string strFinalFile;
+
+ if (getStatistic()->newFilePNG(canvas, strFinalFile))
+ {
+ tos << muT("<img src=\"") << strFinalFile << muT("\"/>");
+ }
+
+ tos << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td class=\"bars_middle\">")
+ << muT("<div class=\"") << m_CSS << muT("\">")
+ << muT("<div class=\"l\"></div>") << ext::endl;
+
+ for (curDay = m_nFirstDay; curDay <= m_nLastDay; curDay += m_nDays)
+ {
+ part_out = 0;
+ part_in = 0;
+
+ for (partDay = 0; partDay < m_nDays; partDay++)
+ {
+ TimelineMap::const_iterator i = pData->find(curDay + partDay);
+
+ if (i != pData->end())
+ {
+ part_out += i->second.out;
+ part_in += i->second.in;
+ }
+ }
+
+ int part_sum = part_in + part_out;
+ int bar_len = 0;
+
+ if (part_sum > 0)
+ {
+ bar_len = -24 + 48 * part_out / part_sum;
+ bar_len += (part_out > part_in) ? 1 : 0;
+ }
+
+ if (m_bDetail)
+ {
+ DWORD rightDay = min(curDay + m_nDays - 1, m_nLastDay);
+
+ tos << muT("<div title=\"");
+
+ if (rightDay != curDay)
+ {
+ tos << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[#{start_date}-#{end_date}] #{out_amount} (out) / #{in_amount} (in)")))
+ % muT("#{start_date}") * utils::timestampToDate(curDay * 86400)
+ % muT("#{end_date}") * utils::timestampToDate(rightDay * 86400)
+ % muT("#{out_amount}") * utils::intToGrouped(part_out)
+ % muT("#{in_amount}") * utils::intToGrouped(part_in)));
+ }
+ else
+ {
+ tos << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[#{date}] #{out_amount} (out) / #{in_amount} (in)")))
+ % muT("#{date}") * utils::timestampToDate(curDay * 86400)
+ % muT("#{out_amount}") * utils::intToGrouped(part_out)
+ % muT("#{in_amount}") * utils::intToGrouped(part_in)));
+ }
+
+ tos << muT("\" style=\"left: ")
+ << (3 * ((curDay - m_nFirstDay) / m_nDays)) << muT("px;\">");
+ }
+ else if (bar_len != 0)
+ {
+ tos << muT("<div style=\"left: ") << (3 * ((curDay - m_nFirstDay) / m_nDays)) << muT("px;\">");
+ }
+
+ if (bar_len < 0)
+ {
+ tos << muT("<div class=\"i\" style=\"height: ") << -bar_len << muT("px;\"></div>");
+ }
+ else if (bar_len > 0)
+ {
+ tos << muT("<div class=\"o\" style=\"top: ") << (25 - bar_len) << muT("px; height: ") << bar_len << muT("px;\"></div>");
+ }
+
+ if (m_bDetail || bar_len != 0)
+ {
+ tos << muT("</div>") << ext::endl;
+ }
+ }
+
+ tos << muT("</div></td>") << ext::endl;
+ }
+}
diff --git a/plugins/HistoryStats/src/column_timeline.h b/plugins/HistoryStats/src/column_timeline.h
new file mode 100644
index 0000000000..642a900546
--- /dev/null
+++ b/plugins/HistoryStats/src/column_timeline.h
@@ -0,0 +1,77 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_TIMELINE_H)
+#define HISTORYSTATS_GUARD_COLUMN_TIMELINE_H
+
+#include "column.h"
+
+/*
+ * ColTimeline
+ */
+
+class ColTimeline
+ : public Column
+{
+public:
+ typedef std::map<int, InOut> TimelineMap;
+
+private:
+ int m_nSource;
+ int m_nSourceType;
+ int m_nIgnoreOld;
+ bool m_bDetail;
+ int m_nDays;
+
+ OptionsCtrl::Combo m_hSource;
+ OptionsCtrl::Edit m_hIgnoreOld;
+ OptionsCtrl::Check m_hDetail;
+ OptionsCtrl::Edit m_hDays;
+
+ ext::string m_CSS;
+ int m_nTimelineWidth;
+ int m_nFirstDay;
+ int m_nLastDay;
+
+private:
+ int getValue(const InOut& value)
+ {
+ switch (m_nSourceType)
+ {
+ case 0: return value.in;
+ case 1: return value.out;
+ case 2: return value.total();
+ }
+
+ return 0;
+ }
+
+ void addToSlot(Contact& contact, bool bOutgoing, DWORD localTimestamp, int toAdd);
+
+ void outputRenderRowInOut(ext::ostream& tos, const Contact& contact, DisplayType display);
+ void outputRenderRowRatio(ext::ostream& tos, const Contact& contact, DisplayType display);
+
+public:
+ explicit ColTimeline();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColTimeline; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Timeline")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding a graphical overview of your chatting behaviour (out, in, total, in/out ratio) from the first to the last day of your history on an daily basis. Multiple days can be grouped. Different chatting behaviour measures are available.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual int impl_getFeatures() const { return cfHasConfig | cfAcquiresData; }
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual int impl_configGetRestrictions(ext::string* pDetails) const;
+ virtual ext::string impl_contactDataGetUID() const;
+ virtual void impl_contactDataPrepare(Contact& contact) const;
+ virtual void impl_contactDataFree(Contact& contact) const;
+ virtual void impl_contactDataAcquireMessage(Contact& contact, Message& msg);
+ virtual void impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration);
+ virtual void impl_contactDataMerge(Contact& contact, const Contact& include) const;
+ virtual void impl_columnDataAfterOmit();
+ virtual StyleList impl_outputGetAdditionalStyles(IDProvider& idp);
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_TIMELINE_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_wordcount.cpp b/plugins/HistoryStats/src/column_wordcount.cpp
new file mode 100644
index 0000000000..fd172e0716
--- /dev/null
+++ b/plugins/HistoryStats/src/column_wordcount.cpp
@@ -0,0 +1,202 @@
+#include "_globals.h"
+#include "column_wordcount.h"
+
+#include <algorithm>
+
+/*
+ * ColWordCount
+ */
+
+ColWordCount::ColWordCount()
+ : m_nVisMode(0), m_bDetail(true),
+ m_hVisMode(NULL), m_hDetail(NULL)
+{
+}
+
+void ColWordCount::impl_copyConfig(const Column* pSource)
+{
+ ColBaseWords::impl_copyConfig(pSource);
+
+ const ColWordCount& src = *reinterpret_cast<const ColWordCount*>(pSource);
+
+ m_nVisMode = src.m_nVisMode;
+ m_bDetail = src.m_bDetail;
+}
+
+void ColWordCount::impl_configRead(const SettingsTree& settings)
+{
+ ColBaseWords::impl_configRead(settings);
+
+ m_nVisMode = settings.readIntRanged(con::KeyVisMode, 0, 0, 2);
+ m_bDetail = settings.readBool(con::KeyDetail, true);
+}
+
+void ColWordCount::impl_configWrite(SettingsTree& settings) const
+{
+ ColBaseWords::impl_configWrite(settings);
+
+ settings.writeInt (con::KeyVisMode, m_nVisMode);
+ settings.writeBool(con::KeyDetail, m_bDetail);
+}
+
+void ColWordCount::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ ColBaseWords::impl_configToUI(Opt, hGroup);
+
+ OptionsCtrl::Group hTemp;
+
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("Word count type")));
+ /**/ m_hVisMode = Opt.insertRadio(hTemp, NULL, i18n(muT("Total words")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Distinct words")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Ratio total/distinct words")));
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("Additional info in tooltip (depends on type)")));
+
+ Opt.setRadioChecked(m_hVisMode, m_nVisMode);
+ Opt.checkItem (m_hDetail , m_bDetail );
+}
+
+void ColWordCount::impl_configFromUI(OptionsCtrl& Opt)
+{
+ ColBaseWords::impl_configFromUI(Opt);
+
+ m_nVisMode = Opt.getRadioChecked(m_hVisMode);
+ m_bDetail = Opt.isItemChecked (m_hDetail );
+}
+
+void ColWordCount::impl_contactDataFree(Contact& contact) const
+{
+ ColBaseWords::impl_contactDataFree(contact);
+
+ size_t* pTrData = reinterpret_cast<size_t*>(contact.getSlot(contactDataTransformSlotGet()));
+
+ if (pTrData)
+ {
+ delete pTrData;
+ contact.setSlot(contactDataTransformSlotGet(), NULL);
+ }
+}
+
+void ColWordCount::impl_contactDataTransform(Contact& contact) const
+{
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+ size_t* pTrData = new size_t[2];
+
+ contact.setSlot(contactDataTransformSlotGet(), pTrData);
+
+ // total words
+ pTrData[0] = 0;
+
+ citer_each_(WordMap, word, *pData)
+ {
+ pTrData[0] += word->second.total();
+ }
+
+ // distinct words
+ pTrData[1] = pData->size();
+}
+
+void ColWordCount::impl_contactDataTransformCleanup(Contact& contact) const
+{
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+
+ if (pData)
+ {
+ pData->clear();
+
+ delete[] pData;
+ contact.setSlot(contactDataSlotGet(), NULL);
+ }
+}
+
+void ColWordCount::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szTypeDesc[] = {
+ I18N(muT("Total word count")),
+ I18N(muT("Distinct word count")),
+ I18N(muT("Ratio total/distinct words")),
+ };
+
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("incoming messages")),
+ I18N(muT("outgoing messages")),
+ I18N(muT("all messages")),
+ };
+
+ if (row == 1)
+ {
+ ext::string strTitle = str(ext::kformat(i18n(muT("#{type} for #{data}")))
+ % muT("#{type}") * i18n(szTypeDesc[m_nVisMode])
+ % muT("#{data}") * i18n(szSourceDesc[m_nSource]));
+
+ writeRowspanTD(tos, getCustomTitle(i18n(szTypeDesc[m_nVisMode]), strTitle), row, 1, rowSpan);
+ }
+}
+
+void ColWordCount::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ const size_t* pWordCount = reinterpret_cast<const size_t*>(contact.getSlot(contactDataTransformSlotGet()));
+
+ switch (m_nVisMode)
+ {
+ case 0:
+ {
+ if (!m_bDetail)
+ {
+ tos << muT("<td class=\"num\">")
+ << utils::intToGrouped(pWordCount[0])
+ << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td class=\"num\" title=\"")
+ << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("#{distict_words} distinct")))
+ % muT("#{distict_words}") * utils::intToGrouped(pWordCount[1])))
+ << muT("\">")
+ << utils::intToGrouped(pWordCount[0])
+ << muT("</td>") << ext::endl;
+ }
+ }
+ break;
+
+ case 1:
+ {
+ if (!m_bDetail)
+ {
+ tos << muT("<td class=\"num\">")
+ << utils::intToGrouped(pWordCount[1])
+ << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td class=\"num\" title=\"")
+ << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("#{words} total")))
+ % muT("#{words}") * utils::intToGrouped(pWordCount[0])))
+ << muT("\">")
+ << utils::intToGrouped(pWordCount[1])
+ << muT("</td>") << ext::endl;
+ }
+ }
+ break;
+
+ default:
+ {
+ if (!m_bDetail)
+ {
+ tos << muT("<td class=\"num\">")
+ << utils::ratioToString(pWordCount[0], pWordCount[1], 2)
+ << muT("</td>") << ext::endl;
+ }
+ else
+ {
+ tos << muT("<td class=\"num\" title=\"")
+ << utils::htmlEscape(ext::str(ext::kformat(i18n(muT("#{words} total / #{distict_words} distinct")))
+ % muT("#{words}") * utils::intToGrouped(pWordCount[0])
+ % muT("#{distict_words}") * utils::intToGrouped(pWordCount[1])))
+ << muT("\">")
+ << utils::ratioToString(pWordCount[0], pWordCount[1], 2)
+ << muT("</td>") << ext::endl;
+ }
+ }
+ break;
+ }
+}
diff --git a/plugins/HistoryStats/src/column_wordcount.h b/plugins/HistoryStats/src/column_wordcount.h
new file mode 100644
index 0000000000..028c95a952
--- /dev/null
+++ b/plugins/HistoryStats/src/column_wordcount.h
@@ -0,0 +1,39 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_WORDCOUNT_H)
+#define HISTORYSTATS_GUARD_COLUMN_WORDCOUNT_H
+
+#include "colbase_words.h"
+
+/*
+ * ColWordCount
+ */
+
+class ColWordCount
+ : public ColBaseWords
+{
+protected:
+ int m_nVisMode;
+ bool m_bDetail;
+
+ OptionsCtrl::Radio m_hVisMode;
+ OptionsCtrl::Check m_hDetail;
+
+public:
+ explicit ColWordCount();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColWordCount; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Word count")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding the number of (distinct) words used by you, by your contact, or by both of you.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual void impl_contactDataFree(Contact& contact) const;
+ virtual void impl_contactDataTransform(Contact& contact) const;
+ virtual void impl_contactDataTransformCleanup(Contact& contact) const;
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_WORDCOUNT_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/column_words.cpp b/plugins/HistoryStats/src/column_words.cpp
new file mode 100644
index 0000000000..61991f3638
--- /dev/null
+++ b/plugins/HistoryStats/src/column_words.cpp
@@ -0,0 +1,300 @@
+#include "_globals.h"
+#include "column_words.h"
+
+#include <algorithm>
+
+/*
+ * ColWords
+ */
+
+void ColWords::generateWords(WordMap* pWords, WordList* pWordList) const
+{
+ int maxCount = min(pWords->size(), m_nOffset + m_nNum);
+ int curWord = 0;
+
+ pWordList->reserve(maxCount);
+
+ if (m_nVisMode == 0)
+ {
+ // most common words
+ MostCommonWords pred;
+
+ while (--maxCount >= 0)
+ {
+ WordMap::iterator i = std::min_element(pWords->begin(), pWords->end(), pred);
+
+ if (++curWord > m_nOffset)
+ {
+ pWordList->push_back(std::make_pair(i->second, i->first));
+ }
+
+ pWords->erase(i);
+ }
+ }
+ else if (m_nVisMode == 1)
+ {
+ // least common words
+ LeastCommonWords pred;
+
+ while (--maxCount >= 0)
+ {
+ WordMap::iterator i = std::min_element(pWords->begin(), pWords->end(), pred);
+
+ if (++curWord > m_nOffset)
+ {
+ pWordList->push_back(std::make_pair(i->second, i->first));
+ }
+
+ pWords->erase(i);
+ }
+ }
+ else // m_nVisMode == 2
+ {
+ // longest words
+ LongestWords pred;
+
+ while (--maxCount >= 0)
+ {
+ WordMap::iterator i = std::min_element(pWords->begin(), pWords->end(), pred);
+
+ if (++curWord > m_nOffset)
+ {
+ pWordList->push_back(std::make_pair(i->second, i->first));
+ }
+
+ pWords->erase(i);
+ }
+ }
+
+ iter_each_(WordList, j, *pWordList)
+ {
+ (*pWords)[j->second] = j->first;
+ }
+}
+
+ColWords::ColWords()
+ : m_nVisMode(0)
+ , m_nNum(10)
+ , m_nOffset(0)
+ , m_bDetail(true)
+ , m_bDetailInOut(false)
+ , m_bInOutColor(false)
+ , m_hVisMode(NULL)
+ , m_hNum(NULL)
+ , m_hOffset(NULL)
+ , m_hDetail(NULL)
+ , m_hDetailInOut(NULL)
+ , m_hInOutColor(NULL)
+{
+}
+
+void ColWords::impl_copyConfig(const Column* pSource)
+{
+ ColBaseWords::impl_copyConfig(pSource);
+
+ const ColWords& src = *reinterpret_cast<const ColWords*>(pSource);
+
+ m_nVisMode = src.m_nVisMode;
+ m_nNum = src.m_nNum;
+ m_nOffset = src.m_nOffset;
+ m_bDetail = src.m_bDetail;
+ m_bDetailInOut = src.m_bDetailInOut;
+ m_bInOutColor = src.m_bInOutColor;
+}
+
+void ColWords::impl_configRead(const SettingsTree& settings)
+{
+ ColBaseWords::impl_configRead(settings);
+
+ m_nVisMode = settings.readIntRanged(con::KeyVisMode, 0, 0, 2);
+ m_nNum = settings.readIntRanged(con::KeyNum, 10, 1, 1000);
+ m_nOffset = settings.readIntRanged(con::KeyOffset, 0, 0, 1000);
+ m_bDetail = settings.readBool(con::KeyDetail, true);
+ m_bDetailInOut = settings.readBool(con::KeyDetailInOut, false);
+ m_bInOutColor = settings.readBool(con::KeyInOutColor, false);
+}
+
+void ColWords::impl_configWrite(SettingsTree& settings) const
+{
+ ColBaseWords::impl_configWrite(settings);
+
+ settings.writeInt (con::KeyVisMode, m_nVisMode);
+ settings.writeInt (con::KeyNum, m_nNum);
+ settings.writeInt (con::KeyOffset, m_nOffset);
+ settings.writeBool(con::KeyDetail, m_bDetail);
+ settings.writeBool(con::KeyDetailInOut, m_bDetailInOut);
+ settings.writeBool(con::KeyInOutColor, m_bInOutColor);
+}
+
+void ColWords::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup)
+{
+ ColBaseWords::impl_configToUI(Opt, hGroup);
+
+ OptionsCtrl::Group hTemp;
+
+ /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("Words type")));
+ /**/ m_hVisMode = Opt.insertRadio(hTemp, NULL, i18n(muT("Most common words")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Least common words")));
+ /**/ Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Longest words")));
+ /**/m_hNum = Opt.insertEdit (hGroup, i18n(muT("Number of words")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/m_hOffset = Opt.insertEdit (hGroup, i18n(muT("Number of words to skip in output")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/m_hDetail = Opt.insertCheck(hGroup, i18n(muT("Word count for each word (tooltip)")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/m_hDetailInOut = Opt.insertCheck(m_hDetail, i18n(muT("Show separate counts for incoming/outgoing")));
+ /**/m_hInOutColor = Opt.insertCheck(hGroup, i18n(muT("Color words according to in/out ratio")));
+
+ Opt.setRadioChecked(m_hVisMode , m_nVisMode );
+ Opt.setEditNumber (m_hNum , m_nNum );
+ Opt.setEditNumber (m_hOffset , m_nOffset );
+ Opt.checkItem (m_hDetail , m_bDetail );
+ Opt.checkItem (m_hDetailInOut, m_bDetailInOut);
+ Opt.checkItem (m_hInOutColor , m_bInOutColor );
+}
+
+void ColWords::impl_configFromUI(OptionsCtrl& Opt)
+{
+ ColBaseWords::impl_configFromUI(Opt);
+
+ m_nVisMode = Opt.getRadioChecked(m_hVisMode );
+ m_nNum = Opt.getEditNumber (m_hNum );
+ m_nOffset = Opt.getEditNumber (m_hOffset );
+ m_bDetail = Opt.isItemChecked (m_hDetail );
+ m_bDetailInOut = Opt.isItemChecked (m_hDetailInOut);
+ m_bInOutColor = Opt.isItemChecked (m_hInOutColor );
+
+ // ensure constraints
+ utils::ensureRange(m_nNum, 1, 1000, 10);
+}
+
+void ColWords::impl_contactDataFree(Contact& contact) const
+{
+ ColBaseWords::impl_contactDataFree(contact);
+
+ WordList* pTrData = reinterpret_cast<WordList*>(contact.getSlot(contactDataTransformSlotGet()));
+
+ if (pTrData)
+ {
+ delete pTrData;
+ contact.setSlot(contactDataTransformSlotGet(), NULL);
+ }
+}
+
+void ColWords::impl_contactDataTransform(Contact& contact) const
+{
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+ WordList* pTrData = new WordList;
+
+ contact.setSlot(contactDataTransformSlotGet(), pTrData);
+
+ generateWords(pData, pTrData);
+}
+
+void ColWords::impl_contactDataTransformCleanup(Contact& contact) const
+{
+ WordMap* pData = reinterpret_cast<WordMap*>(contact.getSlot(contactDataSlotGet()));
+
+ if (pData)
+ {
+ pData->clear();
+
+ delete pData;
+ contact.setSlot(contactDataSlotGet(), NULL);
+ }
+}
+
+Column::StyleList ColWords::impl_outputGetAdditionalStyles(IDProvider& idp)
+{
+ StyleList l;
+
+ if (m_bInOutColor)
+ {
+ l.push_back(StylePair(muT("span.onum"), muT("color: ") + utils::colorToHTML(con::ColorOut) + muT(";")));
+ l.push_back(StylePair(muT("span.inum"), muT("color: ") + utils::colorToHTML(con::ColorIn) + muT(";")));
+ }
+
+ return l;
+}
+
+void ColWords::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const
+{
+ static const mu_text* szTypeDesc[] = {
+ I18N(muT("Most common words")),
+ I18N(muT("Least common words")),
+ I18N(muT("Longest words")),
+ };
+
+ static const mu_text* szSourceDesc[] = {
+ I18N(muT("incoming messages")),
+ I18N(muT("outgoing messages")),
+ I18N(muT("all messages")),
+ };
+
+ if (row == 1)
+ {
+ ext::string strTitle = str(ext::kformat(i18n(muT("#{type} for #{data}")))
+ % muT("#{type}") * i18n(szTypeDesc[m_nVisMode])
+ % muT("#{data}") * i18n(szSourceDesc[m_nSource]));
+
+ writeRowspanTD(tos, getCustomTitle(i18n(szTypeDesc[m_nVisMode]), strTitle), row, 1, rowSpan);
+ }
+}
+
+void ColWords::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display)
+{
+ const WordList* pWords = reinterpret_cast<const WordList*>(contact.getSlot(contactDataTransformSlotGet()));
+
+ tos << muT("<td>");
+
+ vector_each_(j, *pWords)
+ {
+ const Word& w = (*pWords)[j];
+
+ if (m_bDetail)
+ {
+ ext::string strTooltip;
+
+ if (!m_bDetailInOut || m_nSource != 2)
+ {
+ strTooltip = utils::intToString(w.first.total());
+ }
+ else
+ {
+ strTooltip = utils::htmlEscape(ext::str(ext::kformat(i18n(muT("[Out] #{out_words} / [In] #{in_words}")))
+ % muT("#{out_words}") * w.first.out
+ % muT("#{in_words}") * w.first.in));
+ }
+
+ if (!m_bInOutColor || m_nSource != 2 || w.first.in == w.first.out)
+ {
+ tos << muT("<span title=\"") << strTooltip << muT("\">")
+ << utils::htmlEscape(w.second) << muT("</span>");
+ }
+ else
+ {
+ tos << muT("<span class=\"")
+ << ((w.first.out - w.first.in > 0) ? muT("onum") : muT("inum"))
+ << muT("\" title=\"") << strTooltip << muT("\">")
+ << utils::htmlEscape(w.second) << muT("</span>");
+ }
+ }
+ else
+ {
+ if (!m_bInOutColor || m_nSource != 2 || w.first.in == w.first.out)
+ {
+ tos << utils::htmlEscape(w.second);
+ }
+ else
+ {
+ tos << muT("<span class=\"")
+ << ((w.first.out - w.first.in > 0) ? muT("onum") : muT("inum"))
+ << muT("\">") << utils::htmlEscape(w.second) << muT("</span>");
+ }
+ }
+
+ if (j < pWords->size() - 1)
+ {
+ tos << muT(", ");
+ }
+ }
+
+ tos << muT("</td>") << ext::endl;
+}
diff --git a/plugins/HistoryStats/src/column_words.h b/plugins/HistoryStats/src/column_words.h
new file mode 100644
index 0000000000..f6d1284562
--- /dev/null
+++ b/plugins/HistoryStats/src/column_words.h
@@ -0,0 +1,83 @@
+#if !defined(HISTORYSTATS_GUARD_COLUMN_WORDS_H)
+#define HISTORYSTATS_GUARD_COLUMN_WORDS_H
+
+#include "colbase_words.h"
+
+/*
+ * ColWords
+ */
+
+class ColWords
+ : public ColBaseWords
+{
+public:
+ typedef std::pair<InOut, ext::string> Word;
+ typedef std::vector<Word> WordList;
+
+private:
+ class MostCommonWords
+ {
+ public:
+ int operator ()(const WordMap::value_type& a, const WordMap::value_type& b)
+ {
+ return (a.second != b.second) ? (a.second > b.second) : (a.first < b.first);
+ }
+ };
+
+ class LeastCommonWords
+ {
+ public:
+ int operator ()(const WordMap::value_type& a, const WordMap::value_type& b)
+ {
+ return (a.second != b.second) ? (a.second < b.second) : (a.first < b.first);
+ }
+ };
+
+ class LongestWords
+ {
+ public:
+ int operator ()(const WordMap::value_type& a, const WordMap::value_type& b)
+ {
+ return (a.first.length() != b.first.length()) ? (a.first.length() > b.first.length()) : (a.first < b.first);
+ }
+ };
+
+protected:
+ int m_nVisMode;
+ int m_nNum;
+ int m_nOffset;
+ bool m_bDetail;
+ bool m_bDetailInOut;
+ bool m_bInOutColor;
+
+ OptionsCtrl::Radio m_hVisMode;
+ OptionsCtrl::Edit m_hNum;
+ OptionsCtrl::Edit m_hOffset;
+ OptionsCtrl::Check m_hDetail;
+ OptionsCtrl::Check m_hDetailInOut;
+ OptionsCtrl::Check m_hInOutColor;
+
+private:
+ void generateWords(WordMap* pWords, WordList* pWordList) const;
+
+public:
+ explicit ColWords();
+
+protected:
+ virtual const mu_text* impl_getUID() const { return con::ColWords; }
+ virtual const mu_text* impl_getTitle() const { return i18n(muT("Words")); }
+ virtual const mu_text* impl_getDescription() const { return i18n(muT("Column holding a list of a specified number of most/least common words or longest words used by you, by your contact, or by both of you.")); }
+ virtual void impl_copyConfig(const Column* pSource);
+ virtual void impl_configRead(const SettingsTree& settings);
+ virtual void impl_configWrite(SettingsTree& settings) const;
+ virtual void impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup);
+ virtual void impl_configFromUI(OptionsCtrl& Opt);
+ virtual void impl_contactDataFree(Contact& contact) const;
+ virtual void impl_contactDataTransform(Contact& contact) const;
+ virtual void impl_contactDataTransformCleanup(Contact& contact) const;
+ virtual StyleList impl_outputGetAdditionalStyles(IDProvider& idp);
+ virtual void impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const;
+ virtual void impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display);
+};
+
+#endif // HISTORYSTATS_GUARD_COLUMN_WORDS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/contact.cpp b/plugins/HistoryStats/src/contact.cpp
new file mode 100644
index 0000000000..260ed68c1e
--- /dev/null
+++ b/plugins/HistoryStats/src/contact.cpp
@@ -0,0 +1,166 @@
+#include "_globals.h"
+#include "contact.h"
+#include "utils.h"
+
+#include <queue>
+#include <cmath>
+
+Contact::Contact(Statistic* pStatistic, int nSlots, const ext::string& nick, const ext::string& protocol, const ext::string& group, int nContacts, int nSubcontacts)
+ : m_Nick(nick)
+ , m_Protocol(protocol)
+ , m_Group(group)
+ , m_Bytes(0, 0)
+ , m_Messages(0, 0)
+ , m_Chats(0, 0)
+ , m_bFirstLastTimeValid(false)
+ , m_FirstTime(0)
+ , m_LastTime(0)
+ , m_bChatDurValid(false)
+ , m_ChatDurMin(0xFFFFFFFF)
+ , m_ChatDurMax(0)
+ , m_ChatDurSum(0)
+ , m_Urls(0, 0)
+ , m_Files(0, 0)
+ , m_NumContacts(nContacts)
+ , m_NumSubcontacts(nSubcontacts)
+ , m_pStatistic(pStatistic)
+{
+ m_Slots.resize(nSlots, NULL);
+}
+
+void Contact::updateTime(DWORD msgTime)
+{
+ if (!m_bFirstLastTimeValid)
+ {
+ m_FirstTime = msgTime;
+ m_LastTime = msgTime;
+ m_bFirstLastTimeValid = true;
+ }
+ else
+ {
+ m_FirstTime = min(m_FirstTime, msgTime);
+ m_LastTime = max(m_LastTime, msgTime);
+ }
+}
+
+void Contact::updateChatDur(DWORD timeDelta)
+{
+ m_bChatDurValid = true;
+
+ m_ChatDurSum += timeDelta;
+
+ m_ChatDurMin = min(m_ChatDurMin, timeDelta);
+ m_ChatDurMax = max(m_ChatDurMax, timeDelta);
+}
+
+double Contact::getAvg(int nTotal) const
+{
+ DWORD dwHistTime = m_pStatistic->getLastTime() - getFirstTime();
+
+ if (dwHistTime < m_pStatistic->getAverageMinTime())
+ {
+ dwHistTime = m_pStatistic->getAverageMinTime();
+ }
+
+ return dwHistTime ? (0.0 + nTotal) / dwHistTime : 0.0;
+}
+
+void Contact::addMessage(Message& msg)
+{
+ if (msg.isOutgoing())
+ {
+ m_Bytes.out += msg.getLength();
+ m_Messages.out++;
+ }
+ else
+ {
+ m_Bytes.in += msg.getLength();
+ m_Messages.in++;
+ }
+
+ updateTime(msg.getTimestamp());
+}
+
+void Contact::addChat(bool bOutgoing, DWORD localTimestampStarted, DWORD duration)
+{
+ if (bOutgoing)
+ {
+ m_Chats.out++;
+ }
+ else
+ {
+ m_Chats.in++;
+ }
+
+ updateChatDur(duration);
+}
+
+void Contact::addEvent(WORD eventType, bool bOutgoing)
+{
+ InOut* pIO = NULL;
+
+ switch (eventType)
+ {
+ case EVENTTYPE_URL:
+ pIO = &m_Urls;
+ break;
+
+ case EVENTTYPE_FILE:
+ pIO = &m_Files;
+ break;
+
+ default:
+ return;
+ }
+
+ if (bOutgoing)
+ {
+ pIO->out++;
+ }
+ else
+ {
+ pIO->in++;
+ }
+}
+
+void Contact::merge(const Contact& other)
+{
+ if (m_Nick != other.m_Nick)
+ {
+ m_Nick = i18n(muT("(multiple)"));
+ }
+
+ if (m_Protocol != other.m_Protocol)
+ {
+ m_Protocol = i18n(muT("(multiple)"));
+ }
+
+ if (m_Group != other.m_Group)
+ {
+ m_Group = i18n(muT("(multiple)"));
+ }
+
+ m_Bytes += other.m_Bytes;
+ m_Messages += other.m_Messages;
+ m_Chats += other.m_Chats;
+
+ if (other.m_bFirstLastTimeValid)
+ {
+ updateTime(other.m_FirstTime);
+ updateTime(other.m_LastTime);
+ }
+
+ if (other.m_bChatDurValid)
+ {
+ m_ChatDurSum += other.m_ChatDurSum - other.m_ChatDurMin - other.m_ChatDurMax;
+
+ updateChatDur(other.m_ChatDurMin);
+ updateChatDur(other.m_ChatDurMax);
+ }
+
+ m_Files += other.m_Files;
+ m_Urls += other.m_Urls;
+
+ m_NumContacts += other.m_NumContacts;
+ m_NumSubcontacts += other.m_NumSubcontacts;
+}
diff --git a/plugins/HistoryStats/src/contact.h b/plugins/HistoryStats/src/contact.h
new file mode 100644
index 0000000000..a4031269de
--- /dev/null
+++ b/plugins/HistoryStats/src/contact.h
@@ -0,0 +1,226 @@
+#if !defined(HISTORYSTATS_GUARD_CONTACT_H)
+#define HISTORYSTATS_GUARD_CONTACT_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include <string>
+#include <map>
+#include <vector>
+#include <ctime>
+
+#include "inout.h"
+#include "settings.h"
+#include "message.h"
+#include "statistic.h"
+
+/*
+ * Contact
+ */
+
+class Contact
+ : private pattern::NotCopyable<Contact>
+{
+private:
+ ext::string m_Nick;
+ ext::string m_Protocol;
+ ext::string m_Group;
+ InOut m_Bytes;
+ InOut m_Messages;
+ InOut m_Chats;
+ bool m_bChatDurValid;
+ DWORD m_ChatDurMin;
+ DWORD m_ChatDurMax;
+ DWORD m_ChatDurSum;
+ bool m_bFirstLastTimeValid;
+ DWORD m_FirstTime;
+ DWORD m_LastTime;
+ InOut m_Files;
+ InOut m_Urls;
+ int m_NumContacts;
+ int m_NumSubcontacts;
+
+ std::vector<void*> m_Slots;
+ Statistic* m_pStatistic;
+
+private:
+ void updateTime(DWORD msgTime);
+ void updateChatDur(DWORD timeDelta);
+ double getAvg(int nTotal) const;
+
+public:
+ explicit Contact(Statistic* pStatistic, int nSlots, const ext::string& nick, const ext::string& protocol, const ext::string& group, int nContacts, int nSubcontacts);
+
+ // basic contact info
+ const ext::string& getNick() const { return m_Nick; }
+ const ext::string& getProtocol() const { return m_Protocol; }
+ const ext::string& getGroup() const { return m_Group; }
+
+ // characters/messages/bytes (absolute)
+ int getOutBytes() const { return m_Bytes.out; }
+ int getInBytes() const { return m_Bytes.in; }
+ int getTotalBytes() const { return m_Bytes.total(); }
+ int getOutMessages() const { return m_Messages.out; }
+ int getInMessages() const { return m_Messages.in; }
+ int getTotalMessages() const { return m_Messages.total(); }
+ int getOutChats() const { return m_Chats.out; }
+ int getInChats() const { return m_Chats.in; }
+ int getTotalChats() const { return m_Chats.total(); }
+
+ // characters/messages/bytes (average)
+ double getOutBytesAvg() const { return getAvg(m_Bytes.out); }
+ double getInBytesAvg() const { return getAvg(m_Bytes.in); }
+ double getTotalBytesAvg() const { return getAvg(m_Bytes.total()); }
+ double getOutMessagesAvg() const { return getAvg(m_Messages.out); }
+ double getInMessagesAvg() const { return getAvg(m_Messages.in); }
+ double getTotalMessagesAvg() const { return getAvg(m_Messages.total()); }
+ double getOutChatsAvg() const { return getAvg(m_Chats.out); }
+ double getInChatsAvg() const { return getAvg(m_Chats.in); }
+ double getTotalChatsAvg() const { return getAvg(m_Chats.total()); }
+
+ // chat duration
+ bool isChatDurValid() const { return m_bChatDurValid; }
+ DWORD getChatDurMin() const { return m_ChatDurMin; }
+ DWORD getChatDurAvg() const { return getTotalChats() ? m_ChatDurSum / getTotalChats() : 0; }
+ DWORD getChatDurMax() const { return m_ChatDurMax; }
+ int getChatDurMinForSort() const { return m_bChatDurValid ? getChatDurMin() : -1; }
+ int getChatDurAvgForSort() const { return m_bChatDurValid ? getChatDurAvg() : -1; }
+ int getChatDurMaxForSort() const { return m_bChatDurValid ? getChatDurMax() : -1; }
+ DWORD getChatDurSum() const { return m_ChatDurSum; }
+
+ // first/last time
+ bool isFirstLastTimeValid() const { return m_bFirstLastTimeValid; }
+ DWORD getFirstTime() const { return m_FirstTime; }
+ DWORD getLastTime() const { return m_LastTime; }
+
+ // files and URLs
+ int getOutUrls() const { return m_Urls.out; }
+ int getInUrls() const { return m_Urls.in; }
+ int getTotalUrls() const { return m_Urls.total(); }
+ int getOutFiles() const { return m_Files.out; }
+ int getInFiles() const { return m_Files.in; }
+ int getTotalFiles() const { return m_Files.total(); }
+
+ // (sub)contact counts
+ int getNumContacts() const { return m_NumContacts; }
+ int getNumSubcontacts() const { return m_NumSubcontacts; }
+
+ // stuff for reading history
+ void beginMessages() { }
+ void endMessages() { }
+ void addMessage(Message& msg);
+ void addChat(bool bOutgoing, DWORD localTimestampStarted, DWORD duration);
+ void addEvent(WORD eventType, bool bOutgoing);
+ void merge(const Contact& other);
+
+ // slot stuff
+ int countSlot() const { return m_Slots.size(); }
+ const void* getSlot(int index) const { return m_Slots[index]; }
+ void* getSlot(int index) { return m_Slots[index]; }
+ void setSlot(int index, void* pData) { m_Slots[index] = pData; }
+};
+
+/*
+ * ContactCompareBase
+ */
+
+class ContactCompareBase
+{
+protected:
+ bool m_bAsc;
+
+public:
+ virtual bool cmp(const Contact& first, const Contact& second) { return m_bAsc; }
+ void setDir(bool bAsc) { m_bAsc = bAsc; }
+ explicit ContactCompareBase() : m_bAsc(true) { }
+};
+
+/*
+ * ContactCompare<T_>
+ */
+
+template<typename T_>
+class ContactCompare
+ : public ContactCompareBase
+{
+private:
+ T_ (Contact::*m_getData)() const;
+ ContactCompareBase* m_pNextCmp;
+
+public:
+ virtual bool cmp(const Contact& first, const Contact& second)
+ {
+ T_ firstVal = (first.*m_getData)();
+ T_ secondVal = (second.*m_getData)();
+
+ if (firstVal == secondVal)
+ {
+ return m_pNextCmp->cmp(first, second);
+ }
+ else
+ {
+ return (m_bAsc ? (firstVal < secondVal) : (firstVal > secondVal));
+ }
+ }
+
+public:
+ explicit ContactCompare(ContactCompareBase* pNextCmp, T_ (Contact::*getData)() const)
+ : m_pNextCmp(pNextCmp)
+ {
+ m_getData = getData;
+ }
+};
+
+/*
+ * ContactCompareStr
+ */
+
+class ContactCompareStr
+ : public ContactCompareBase
+{
+private:
+ const ext::string& (Contact::*m_getData)() const;
+ ContactCompareBase* m_pNextCmp;
+
+public:
+ virtual bool cmp(const Contact& first, const Contact& second)
+ {
+ const ext::string& firstVal = (first.*m_getData)();
+ const ext::string& secondVal = (second.*m_getData)();
+
+ // int cmpRes = ext::strfunc::icmp(firstVal.c_str(), secondVal.c_str());
+ int cmpRes = ext::strfunc::icoll(firstVal.c_str(), secondVal.c_str());
+
+ if (cmpRes == 0)
+ {
+ return m_pNextCmp->cmp(first, second);
+ }
+ else
+ {
+ return (m_bAsc ? (cmpRes < 0) : (cmpRes > 0));
+ }
+ }
+
+public:
+ explicit ContactCompareStr(ContactCompareBase* pNextCmp, const ext::string& (Contact::*getData)() const)
+ : m_pNextCmp(pNextCmp)
+ {
+ m_getData = getData;
+ }
+};
+
+/*
+ * ContactCompareOp
+ */
+
+class ContactCompareOp
+{
+private:
+ ContactCompareBase* m_pCmp;
+
+public:
+ bool operator ()(const Contact* first, const Contact* second) { return m_pCmp->cmp(*first, *second); }
+ explicit ContactCompareOp(ContactCompareBase* pCmp) : m_pCmp(pCmp) { }
+};
+
+#endif // HISTORYSTATS_GUARD_CONTACT_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/dlgconfigure.cpp b/plugins/HistoryStats/src/dlgconfigure.cpp
new file mode 100644
index 0000000000..5c4ad0b727
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgconfigure.cpp
@@ -0,0 +1,232 @@
+#include "_globals.h"
+#include "dlgconfigure.h"
+
+#include "main.h"
+#include "utils.h"
+#include "dlgoption.h"
+#include "resource.h"
+
+/*
+ * DlgConfigure
+ */
+
+HWND DlgConfigure::m_hCfgWnd = NULL;
+bool DlgConfigure::m_bHookedEvent = false;
+
+INT_PTR CALLBACK DlgConfigure::staticConfigureProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ DlgConfigure* pDlg = reinterpret_cast<DlgConfigure*>(GetWindowLong(hDlg, DWLP_USER));
+
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ pDlg = new DlgConfigure(hDlg);
+ SetWindowLong(hDlg, DWLP_USER, reinterpret_cast<LONG>(pDlg));
+ pDlg->onWMInitDialog();
+ return TRUE;
+
+ case WM_DESTROY:
+ delete pDlg;
+ SetWindowLong(hDlg, DWLP_USER, 0);
+ break;
+
+ case PSM_CHANGED:
+ {
+ EnableWindow(GetDlgItem(hDlg, IDC_APPLY), TRUE);
+ pDlg->m_bChanged = true;
+ }
+ return TRUE;
+
+ case WM_COMMAND:
+ {
+ switch (LOWORD(wParam))
+ {
+ case IDCANCEL:
+ pDlg->onCancel();
+ return TRUE;
+
+ case IDOK:
+ pDlg->onApply();
+ DestroyWindow(hDlg);
+ return TRUE;
+
+ case IDC_APPLY:
+ pDlg->onApply();
+ return TRUE;
+ }
+ }
+ break;
+
+ case WM_WINDOWPOSCHANGED:
+ pDlg->rearrangeControls();
+ return TRUE;
+
+ case WM_GETMINMAXINFO:
+ {
+ static POINT sizeMin = { 0, 0 };
+ MINMAXINFO* pMMI = reinterpret_cast<MINMAXINFO*>(lParam);
+
+ if (sizeMin.x == 0)
+ {
+ RECT rectWin;
+
+ GetWindowRect(hDlg, &rectWin);
+ sizeMin.x = rectWin.right - rectWin.left;
+ sizeMin.y = rectWin.bottom - rectWin.top;
+ }
+
+ pMMI->ptMinTrackSize = sizeMin;
+ }
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+int DlgConfigure::staticEventPreShutdown(WPARAM wParam, LPARAM lParam)
+{
+ if (IsWindow(m_hCfgWnd))
+ {
+ DestroyWindow(m_hCfgWnd);
+ }
+
+ return 0;
+}
+
+void DlgConfigure::showModal()
+{
+ if (g_bConfigureLock)
+ {
+ MessageBox(
+ 0,
+ i18n(muT("You can't access the stand-alone configuration dialog of HistoryStats as long as the options dialog of Miranda IM is open. Please close the options dialog and try again.\r\n\r\nNote that the options offered by both dialogs are the same.")),
+ i18n(muT("HistoryStats - Warning")),
+ MB_ICONWARNING | MB_OK);
+
+ return;
+ }
+
+ if (IsWindow(m_hCfgWnd))
+ SetForegroundWindow(m_hCfgWnd);
+ else
+ CreateDialog(g_hInst, MAKEINTRESOURCE(IDD_CONFIGURE), NULL, staticConfigureProc);
+}
+
+void DlgConfigure::onWMInitDialog()
+{
+ TranslateDialogDefault(m_hWnd);
+ utils::centerDialog(m_hWnd);
+ SendMessage(m_hWnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_HISTORYSTATS))));
+ EnableWindow(GetDlgItem(m_hWnd, IDC_APPLY), FALSE);
+
+ m_hOptWnd = CreateDialogA(g_hInst, MAKEINTRESOURCEA(IDD_OPTIONS), m_hWnd, DlgOption::staticDlgProc);
+
+ ShowWindow(m_hOptWnd, SW_SHOW);
+ SetFocus(m_hOptWnd);
+}
+
+void DlgConfigure::onCancel()
+{
+ PSHNOTIFY pshn;
+
+ pshn.hdr.idFrom = 0;
+ pshn.hdr.code = PSN_RESET;
+ pshn.hdr.hwndFrom = m_hOptWnd;
+ pshn.lParam = 0;
+
+ SendMessage(m_hOptWnd, WM_NOTIFY, 0, reinterpret_cast<LPARAM>(&pshn));
+
+ rearrangeControls();
+
+ DestroyWindow(m_hWnd);
+}
+
+void DlgConfigure::onApply()
+{
+ EnableWindow(GetDlgItem(m_hWnd, IDC_APPLY), FALSE);
+
+ PSHNOTIFY pshn;
+
+ pshn.hdr.idFrom = 0;
+ pshn.hdr.code = PSN_KILLACTIVE;
+ pshn.hdr.hwndFrom = m_hOptWnd;
+ pshn.lParam = 0;
+
+ SendMessage(m_hOptWnd, WM_NOTIFY, 0, reinterpret_cast<LPARAM>(&pshn));
+
+ if (m_bChanged)
+ {
+ m_bChanged = false;
+ pshn.hdr.code = PSN_APPLY;
+
+ SendMessage(m_hOptWnd, WM_NOTIFY, 0, reinterpret_cast<LPARAM>(&pshn));
+ }
+}
+
+void DlgConfigure::rearrangeControls()
+{
+ RECT rClient;
+
+ GetClientRect(m_hWnd, &rClient);
+
+ if (m_nPadY == -1)
+ {
+ RECT rButton = utils::getWindowRect(m_hWnd, IDOK);
+
+ m_nPadY = rClient.bottom - rButton.bottom;
+ m_nOKPadX = rClient.right - rButton.right;
+ m_nCancelPadX = rClient.right - utils::getWindowRect(m_hWnd, IDCANCEL).right;
+ m_nApplyPadX = rClient.right - utils::getWindowRect(m_hWnd, IDC_APPLY).right;
+ }
+
+ RECT rButton;
+
+ rButton = utils::getWindowRect(m_hWnd, IDOK);
+ OffsetRect(&rButton, rClient.right - rButton.right - m_nOKPadX, rClient.bottom - rButton.bottom - m_nPadY);
+ utils::moveWindow(m_hWnd, IDOK, rButton);
+
+ rButton = utils::getWindowRect(m_hWnd, IDCANCEL);
+ OffsetRect(&rButton, rClient.right - rButton.right - m_nCancelPadX, rClient.bottom - rButton.bottom - m_nPadY);
+ utils::moveWindow(m_hWnd, IDCANCEL, rButton);
+
+ rButton = utils::getWindowRect(m_hWnd, IDC_APPLY);
+ OffsetRect(&rButton, rClient.right - rButton.right - m_nApplyPadX, rClient.bottom - rButton.bottom - m_nPadY);
+ utils::moveWindow(m_hWnd, IDC_APPLY, rButton);
+
+ RECT rPage = utils::getWindowRect(m_hWnd, m_hOptWnd);
+
+ rPage.top = m_nPadY;
+ rPage.left = m_nApplyPadX;
+ rPage.right = rClient.right - rClient.left - m_nApplyPadX;
+ rPage.bottom = rButton.top - m_nPadY;
+
+ utils::moveWindow(m_hOptWnd, rPage);
+
+ InvalidateRect(m_hWnd, NULL, TRUE);
+}
+
+DlgConfigure::DlgConfigure(HWND hWnd)
+ : m_hWnd(hWnd), m_bChanged(false), m_hOptWnd(NULL),
+ m_nPadY(-1), m_nOKPadX(0), m_nCancelPadX(0), m_nApplyPadX(0)
+{
+ if (!m_hCfgWnd)
+ {
+ m_hCfgWnd = hWnd;
+
+ if (!m_bHookedEvent)
+ {
+ HookEvent(ME_SYSTEM_PRESHUTDOWN, staticEventPreShutdown);
+ m_bHookedEvent = true;
+ }
+ }
+}
+
+DlgConfigure::~DlgConfigure()
+{
+ DestroyWindow(m_hOptWnd);
+
+ if (m_hWnd == m_hCfgWnd)
+ {
+ m_hCfgWnd = NULL;
+ }
+} \ No newline at end of file
diff --git a/plugins/HistoryStats/src/dlgconfigure.h b/plugins/HistoryStats/src/dlgconfigure.h
new file mode 100644
index 0000000000..d1f8b1c22f
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgconfigure.h
@@ -0,0 +1,41 @@
+#if !defined(HISTORYSTATS_GUARD_DLGCONFIGURE_H)
+#define HISTORYSTATS_GUARD_DLGCONFIGURE_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+class DlgConfigure
+ : private pattern::NotCopyable<DlgConfigure>
+{
+private:
+ static HWND m_hCfgWnd;
+ static bool m_bHookedEvent;
+
+private:
+ static INT_PTR CALLBACK staticConfigureProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+ static int staticEventPreShutdown(WPARAM wParam, LPARAM lParam);
+
+public:
+ static void showModal();
+
+private:
+ HWND m_hWnd;
+ bool m_bChanged;
+ HWND m_hOptWnd;
+ int m_nPadY;
+ int m_nOKPadX;
+ int m_nCancelPadX;
+ int m_nApplyPadX;
+
+private:
+ void onWMInitDialog();
+ void onCancel();
+ void onApply();
+ void rearrangeControls();
+
+private:
+ explicit DlgConfigure(HWND hWnd);
+ ~DlgConfigure();
+};
+
+#endif // HISTORYSTATS_GUARD_DLG_CONFIGURE_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/dlgfilterwords.cpp b/plugins/HistoryStats/src/dlgfilterwords.cpp
new file mode 100644
index 0000000000..5609880d9d
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgfilterwords.cpp
@@ -0,0 +1,387 @@
+#include "_globals.h"
+#include "dlgfilterwords.h"
+
+#include "main.h"
+#include "resource.h"
+
+#include <algorithm>
+
+INT_PTR CALLBACK DlgFilterWords::staticDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ DlgFilterWords* pDlg = reinterpret_cast<DlgFilterWords*>(GetWindowLong(hDlg, DWLP_USER));
+
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ pDlg = reinterpret_cast<DlgFilterWords*>(lParam);
+ SetWindowLong(hDlg, DWLP_USER, reinterpret_cast<LONG>(pDlg));
+ pDlg->m_hWnd = hDlg;
+ pDlg->onWMInitDialog();
+ return TRUE;
+
+ case WM_DESTROY:
+ pDlg->onWMDestroy();
+ pDlg->m_hWnd = NULL;
+ SetWindowLong(hDlg, DWLP_USER, 0);
+ return TRUE;
+
+ case WM_COMMAND:
+ switch (LOWORD(wParam))
+ {
+ case IDOK:
+ EndDialog(hDlg, 1);
+ return TRUE;
+
+ case IDCANCEL:
+ EndDialog(hDlg, 0);
+ return TRUE;
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+ switch (reinterpret_cast<NMHDR*>(lParam)->idFrom)
+ {
+ case IDC_BAND:
+ {
+ BandCtrl::NMBANDCTRL* pNM = reinterpret_cast<BandCtrl::NMBANDCTRL*>(lParam);
+
+ if (pNM->hdr.code == BandCtrl::BCN_CLICKED)
+ {
+ pDlg->onBandClicked(pNM->hButton, pNM->dwData);
+ }
+ }
+ break;
+
+ case IDC_SETS:
+ {
+ OptionsCtrl::NMOPTIONSCTRL * pNM = reinterpret_cast<OptionsCtrl::NMOPTIONSCTRL*>(lParam);
+
+ if (pNM->hdr.code == OptionsCtrl::OCN_MODIFIED)
+ {
+ pDlg->onSetItemModified(pNM->hItem, pNM->dwData);
+ }
+ else if (pNM->hdr.code == OptionsCtrl::OCN_SELCHANGED)
+ {
+ pDlg->onSetSelChanged(pNM->hItem, pNM->dwData);
+ }
+ else if (pNM->hdr.code == OptionsCtrl::OCN_SELCHANGING)
+ {
+ pDlg->onSetSelChanging(pNM->hItem, pNM->dwData);
+ }
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgFilterWords::onWMInitDialog()
+{
+ TranslateDialogDefault(m_hWnd);
+
+ SendMessage(m_hWnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_HISTORYSTATS))));
+
+ utils::centerDialog(m_hWnd);
+
+ // init band control
+ m_Band << GetDlgItem(m_hWnd, IDC_BAND);
+
+ static const struct {
+ WORD iconId;
+ mu_text* szTooltip;
+ bool bDisabled;
+ } columnBand[] = {
+ { IDI_COL_ADD , I18N(muT("Add set")) , false },
+ { IDI_COL_DEL , I18N(muT("Delete set")), true },
+ };
+
+ array_each_(i, columnBand)
+ {
+ HICON hIcon = reinterpret_cast<HICON>(LoadImage(g_hInst, MAKEINTRESOURCE(columnBand[i].iconId), IMAGE_ICON, OS::smIconCX(), OS::smIconCY(), 0));
+ DWORD dwFlags = (columnBand[i].bDisabled ? BandCtrl::BCF_DISABLED : 0);
+
+ m_hActionButtons[i] = m_Band.addButton(dwFlags, hIcon, i, i18n(columnBand[i].szTooltip));
+
+ DestroyIcon(hIcon);
+ }
+
+ // init options control (sets)
+ m_Sets << GetDlgItem(m_hWnd, IDC_SETS);
+
+ citer_each_(FilterVec, i, m_Filters)
+ {
+ const Filter* pFilter = *i;
+
+ HANDLE hCheck = m_Sets.insertCheck(NULL, pFilter->getName().c_str(), 0, reinterpret_cast<DWORD>(pFilter));
+
+ if (m_bColProvided && m_ColFilters.find(pFilter->getID()) != m_ColFilters.end())
+ {
+ m_Sets.checkItem(hCheck, true);
+ }
+ }
+
+ // init other controls
+ static const mu_text* szWordFilterModes[] = {
+ I18N(muT("Filter words matching")),
+ I18N(muT("Filter words containing")),
+ I18N(muT("Filter words starting with")),
+ I18N(muT("Filter words ending with")),
+ I18N(muT("Filter messages matching")),
+ I18N(muT("Filter messages containing")),
+ I18N(muT("Filter messages starting with")),
+ I18N(muT("Filter messages ending with")),
+ };
+
+ array_each_(i, szWordFilterModes)
+ {
+ SendDlgItemMessage(m_hWnd, IDC_MODE, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(i18n(szWordFilterModes[i])));
+ }
+
+ SendDlgItemMessage(m_hWnd, IDC_MODE, CB_SETCURSEL, 0, 0);
+
+ onSetSelChanged(NULL, 0);
+}
+
+void DlgFilterWords::onWMDestroy()
+{
+ // avoid OCN_SELCHANGING messages when dialog object already destroyed
+ m_Sets.deleteAllItems();
+}
+
+void DlgFilterWords::onBandClicked(HANDLE hButton, DWORD dwData)
+{
+ switch (dwData)
+ {
+ case saAdd:
+ onSetAdd();
+ break;
+
+ case saDel:
+ onSetDel();
+ break;
+ }
+}
+
+void DlgFilterWords::onSetAdd()
+{
+ Filter* pFilter = new Filter(utils::getGUID());
+
+ m_Filters.push_back(pFilter);
+
+ HANDLE hAdded = m_Sets.insertCheck(NULL, pFilter->getName().c_str(), 0, reinterpret_cast<DWORD>(pFilter));
+
+ m_Sets.selectItem(hAdded);
+}
+
+void DlgFilterWords::onSetDel()
+{
+ HANDLE hSel = m_Sets.getSelection();
+
+ if (hSel)
+ {
+ Filter* pFilter = reinterpret_cast<Filter*>(m_Sets.getItemData(hSel));
+
+ if (pFilter->getRef() > 0)
+ {
+ if (IDYES != MessageBox(
+ m_hWnd,
+ i18n(muT("The selected set is in use by at least one other column. If you remove it it won't be available to all other columns that use it. Are you sure you want to remove the set?")),
+ i18n(muT("HistoryStats - Warning")),
+ MB_ICONWARNING | MB_YESNO))
+ {
+ return;
+ }
+ }
+
+ FilterVec::iterator i = std::find(m_Filters.begin(), m_Filters.end(), pFilter);
+
+ if (i != m_Filters.end())
+ {
+ m_Sets.deleteItem(hSel);
+ m_Filters.erase(i);
+
+ delete pFilter;
+ }
+ }
+}
+
+void DlgFilterWords::onSetItemModified(HANDLE hItem, DWORD dwData)
+{
+ if (m_bColProvided)
+ {
+ Filter* pFilter = reinterpret_cast<Filter*>(dwData);
+
+ if (m_Sets.isItemChecked(hItem))
+ {
+ m_ColFilters.insert(pFilter->getID());
+ }
+ else
+ {
+ m_ColFilters.erase(pFilter->getID());
+ }
+ }
+ else
+ {
+ if (m_Sets.isItemChecked(hItem))
+ {
+ m_Sets.checkItem(hItem, false);
+ }
+ }
+}
+
+void DlgFilterWords::onSetSelChanging(HANDLE hItem, DWORD dwData)
+{
+ if (hItem)
+ {
+ Filter* pFilter = reinterpret_cast<Filter*>(dwData);
+
+ // set name
+ HWND hText = GetDlgItem(m_hWnd, IDC_SETNAME);
+ int nLen = GetWindowTextLength(hText);
+ mu_text* szText = new mu_text[nLen + 1];
+
+ if (GetWindowText(hText, szText, nLen + 1))
+ {
+ pFilter->setName(szText);
+ m_Sets.setItemLabel(hItem, szText);
+ }
+
+ delete[] szText;
+
+ // words
+ hText = GetDlgItem(m_hWnd, IDC_WORDS);
+ nLen = GetWindowTextLength(hText);
+ szText = new mu_text[nLen + 1];
+
+ if (GetWindowText(hText, szText, nLen + 1))
+ {
+ ext::string strText = szText;
+
+ utils::replaceAllInPlace(strText, muT("\r"), muT(""));
+ pFilter->clearWords();
+
+ ext::string::size_type nPos = strText.find(muC('\n'));
+
+ while (nPos != ext::string::npos)
+ {
+ if (nPos > 0)
+ {
+ pFilter->addWord(utils::toLowerCase(strText.substr(0, nPos)));
+ }
+
+ strText.erase(0, nPos + 1);
+
+ nPos = strText.find(muC('\n'));
+ }
+
+ if (!strText.empty())
+ {
+ pFilter->addWord(utils::toLowerCase(strText));
+ }
+ }
+
+ delete[] szText;
+
+ // mode
+ pFilter->setMode(SendDlgItemMessage(m_hWnd, IDC_MODE, CB_GETCURSEL, 0, 0));
+ }
+}
+
+void DlgFilterWords::onSetSelChanged(HANDLE hItem, DWORD dwData)
+{
+ BOOL bEnable = hItem ? TRUE : FALSE;
+
+ EnableWindow(GetDlgItem(m_hWnd, IDC_SETNAME), bEnable);
+ EnableWindow(GetDlgItem(m_hWnd, IDC_MODE), bEnable);
+ EnableWindow(GetDlgItem(m_hWnd, IDC_WORDS), bEnable);
+
+ if (hItem)
+ {
+ Filter* pFilter = reinterpret_cast<Filter*>(dwData);
+
+ SetDlgItemText(m_hWnd, IDC_SETNAME, pFilter->getName().c_str());
+ SendDlgItemMessage(m_hWnd, IDC_MODE, CB_SETCURSEL, pFilter->getMode(), 0);
+
+ ext::string strWords;
+
+ citer_each_(WordSet, i, pFilter->getWords())
+ {
+ strWords += *i;
+ strWords += muT("\r\n");
+ }
+
+ if (!strWords.empty())
+ {
+ strWords.erase(strWords.length() - 2, 2);
+ }
+
+ SetDlgItemText(m_hWnd, IDC_WORDS, strWords.c_str());
+ }
+ else
+ {
+ SetDlgItemText(m_hWnd, IDC_SETNAME, muT(""));
+ SetDlgItemText(m_hWnd, IDC_WORDS, muT(""));
+ }
+
+ // (de)activate band controls
+ m_Band.enableButton(m_hActionButtons[saDel], bool_(hItem));
+}
+
+void DlgFilterWords::clearFilters()
+{
+ citer_each_(FilterVec, i, m_Filters)
+ {
+ delete *i;
+ }
+
+ m_Filters.clear();
+}
+
+DlgFilterWords::DlgFilterWords()
+ : m_hWnd(NULL), m_bColProvided(false)
+{
+}
+
+DlgFilterWords::~DlgFilterWords()
+{
+ clearFilters();
+}
+
+bool DlgFilterWords::showModal(HWND hParent)
+{
+ return (DialogBoxParam(g_hInst, MAKEINTRESOURCE(IDD_FILTERWORDS), hParent, staticDlgProc, reinterpret_cast<LPARAM>(this)) == 1);
+}
+
+void DlgFilterWords::setFilters(const FilterSet& Filters)
+{
+ clearFilters();
+
+ citer_each_(Settings::FilterSet, i, Filters)
+ {
+ m_Filters.push_back(new Filter(*i));
+ }
+
+ FilterCompare cmp;
+
+ std::sort(m_Filters.begin(), m_Filters.end(), cmp);
+}
+
+void DlgFilterWords::setColFilters(const ColFilterSet& ColFilters)
+{
+ m_bColProvided = true;
+ m_ColFilters = ColFilters;
+}
+
+void DlgFilterWords::updateFilters(FilterSet& Filters)
+{
+ Filters.clear();
+
+ citer_each_(FilterVec, i, m_Filters)
+ {
+ Filters.insert(**i);
+ }
+}
diff --git a/plugins/HistoryStats/src/dlgfilterwords.h b/plugins/HistoryStats/src/dlgfilterwords.h
new file mode 100644
index 0000000000..23fb6a5a9c
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgfilterwords.h
@@ -0,0 +1,66 @@
+#if !defined(HISTORYSTATS_GUARD_DLGFILTERWORDS_H)
+#define HISTORYSTATS_GUARD_DLGFILTERWORDS_H
+
+#include "_globals.h"
+
+#include "bandctrl.h"
+#include "optionsctrl.h"
+#include "settings.h"
+
+class DlgFilterWords
+ : private pattern::NotCopyable<DlgFilterWords>
+{
+private:
+ enum SetAction {
+ saAdd = 0,
+ saDel = 1,
+ };
+
+ typedef Settings::WordSet WordSet;
+ typedef Settings::Filter Filter;
+ typedef Settings::FilterSet FilterSet;
+ typedef Settings::ColFilterSet ColFilterSet;
+ typedef std::vector<Filter*> FilterVec;
+
+ class FilterCompare
+ {
+ public:
+ bool operator ()(const Filter* first, const Filter* second) { return ext::strfunc::icoll(first->getName().c_str(), second->getName().c_str()) < 0; }
+ };
+
+private:
+ static INT_PTR CALLBACK staticDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+
+private:
+ HWND m_hWnd;
+ BandCtrl m_Band;
+ HANDLE m_hActionButtons[2];
+ OptionsCtrl m_Sets;
+ FilterVec m_Filters;
+ bool m_bColProvided;
+ ColFilterSet m_ColFilters;
+
+private:
+ void onWMInitDialog();
+ void onWMDestroy();
+ void onBandClicked(HANDLE hButton, DWORD dwData);
+ void onSetAdd();
+ void onSetDel();
+ void onSetItemModified(HANDLE hItem, DWORD dwData);
+ void onSetSelChanging(HANDLE hItem, DWORD dwData);
+ void onSetSelChanged(HANDLE hItem, DWORD dwData);
+
+ void clearFilters();
+
+public:
+ explicit DlgFilterWords();
+ ~DlgFilterWords();
+
+ bool showModal(HWND hParent);
+ void setFilters(const FilterSet& Filters);
+ void setColFilters(const ColFilterSet& ColFilters);
+ void updateFilters(FilterSet& Filters);
+ const ColFilterSet& getColFilters() { return m_ColFilters; }
+};
+
+#endif // HISTORYSTATS_GUARD_DLGFILTERWORDS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/dlgoption.cpp b/plugins/HistoryStats/src/dlgoption.cpp
new file mode 100644
index 0000000000..e48b3bdb7a
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption.cpp
@@ -0,0 +1,399 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+#include "bandctrl.h"
+#include "statistic.h"
+#include "main.h"
+
+/*
+ * DlgOption
+ */
+
+INT_PTR CALLBACK DlgOption::staticDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ DlgOption* pDlg = reinterpret_cast<DlgOption*>(GetWindowLong(hDlg, DWLP_USER));
+
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ pDlg = new DlgOption(hDlg);
+ SetWindowLong(hDlg, DWLP_USER, reinterpret_cast<LONG>(pDlg));
+ pDlg->onWMInitDialog();
+ return TRUE;
+
+ case WM_DESTROY:
+ delete pDlg;
+ SetWindowLong(hDlg, DWLP_USER, 0);
+ break;
+
+ case WM_WINDOWPOSCHANGED:
+ pDlg->rearrangeControls();
+ return TRUE;
+
+ case WM_NOTIFY:
+ {
+ NMHDR* p = reinterpret_cast<NMHDR*>(lParam);
+
+ switch (p->idFrom)
+ {
+ case 0:
+ if (p->code == PSN_APPLY)
+ {
+ pDlg->saveSettings();
+ }
+ break;
+
+ case IDC_BAND:
+ {
+ if (p->code == BandCtrl::BCN_CLICKED)
+ {
+ BandCtrl::NMBANDCTRL* pNM = reinterpret_cast<BandCtrl::NMBANDCTRL*>(lParam);
+
+ pDlg->onBandClicked(pNM->hButton, pNM->dwData);
+ }
+ else if (p->code == BandCtrl::BCN_DROPDOWN)
+ {
+ BandCtrl::NMBANDCTRL* pNM = reinterpret_cast<BandCtrl::NMBANDCTRL*>(lParam);
+
+ pDlg->onBandDropDown(pNM->hButton, pNM->dwData);
+ }
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgOption::onWMInitDialog()
+{
+ TranslateDialogDefault(m_hWnd);
+
+ m_bSettingsModified = true;
+
+ // init band control
+ m_Band << GetDlgItem(m_hWnd, IDC_BAND);
+
+ if (!g_bConfigureLock)
+ {
+ // lock configure dialog for us
+ g_bConfigureLock = true;
+ m_bAcquiredLock = true;
+
+ // let band spread buttons equally
+ m_Band.setLayout(7);
+
+ // init pages
+ m_pPage[opGlobal] = new SubGlobal();
+ m_pPage[opExclude] = new SubExclude();
+ m_pPage[opInput] = new SubInput();
+ m_pPage[opColumns] = new SubColumns();
+ m_pPage[opOutput] = new SubOutput();
+
+ RECT rPage, rBand;
+
+ GetClientRect(m_hWnd, &rPage);
+ GetClientRect(m_Band, &rBand);
+
+ rPage.top += rBand.bottom;
+
+ static const struct {
+ WORD iconId;
+ mu_text* szText;
+ mu_text* szTooltip;
+ bool bRight;
+ WORD dlgId;
+ bool bVisible;
+ bool bDropDown;
+ } pageBand[] = {
+ { IDI_SETT_GLOBAL , I18N(muT("Global")) , I18N(muT("Global settings")) , false, IDD_SUB_GLOBAL , true , false },
+ { IDI_SETT_EXCLUDE , I18N(muT("Exclude")), I18N(muT("Exclude contacts")) , false, IDD_SUB_EXCLUDE, true , false },
+ { IDI_SETT_INPUT , I18N(muT("Input")) , I18N(muT("Input settings")) , false, IDD_SUB_INPUT , true , false },
+ { IDI_SETT_COLUMNS , I18N(muT("Columns")), I18N(muT("Column settings")) , false, IDD_SUB_COLUMNS, true , false },
+ { IDI_SETT_OUTPUT , I18N(muT("Output")) , I18N(muT("Output settings")) , false, IDD_SUB_OUTPUT , true , false },
+ { IDI_CREATE , I18N(muT("Create")) , I18N(muT("Create statistics")) , true , 0 , true , true },
+ { IDI_CREATE_WARNING, I18N(muT("Create")) , I18N(muT("Create statistics (there are warnings)")), true , 0 , false, true },
+ };
+
+ array_each_(i, pageBand)
+ {
+ HICON hIcon = reinterpret_cast<HICON>(LoadImage(g_hInst, MAKEINTRESOURCE(pageBand[i].iconId), IMAGE_ICON, 32, 32, 0));
+ DWORD dwFlags = 0;
+
+ dwFlags |= pageBand[i].bRight ? BandCtrl::BCF_RIGHT : 0;
+ dwFlags |= (i == m_nCurPage) ? BandCtrl::BCF_CHECKED : 0;
+ dwFlags |= pageBand[i].bVisible ? 0 : BandCtrl::BCF_HIDDEN;
+ dwFlags |= pageBand[i].bDropDown ? BandCtrl::BCF_DROPDOWN : 0;
+
+ HANDLE hButton = m_Band.addButton(dwFlags, hIcon, i, i18n(pageBand[i].szTooltip), i18n(pageBand[i].szText));
+
+ DestroyIcon(hIcon);
+
+ if (i >= opFirstPage && i <= opLastPage)
+ {
+ m_hPageButton[i] = hButton;
+ m_pPage[i]->createWindow(this, pageBand[i].dlgId, rPage);
+ }
+ else if (i == opCreate)
+ {
+ m_hCreateButton = hButton;
+ }
+ else if (i == opCreateWarn)
+ {
+ m_hCreateWarnButton = hButton;
+ }
+ }
+
+ // init first tab
+ int nNewPage = g_pSettings->getLastPage();
+
+ if (nNewPage < opFirstPage || nNewPage > opLastPage)
+ {
+ nNewPage = opGlobal;
+ }
+
+ onBandClicked(m_hPageButton[nNewPage], m_Band.getButtonData(m_hPageButton[nNewPage]));
+
+ // show/hide info about possible problems with PNG output
+ updateProblemInfo();
+ }
+ else
+ {
+ // inform the user that he can't simultaneously access the options with two dialogs
+ ShowWindow(m_Band, SW_HIDE);
+ ShowWindow(GetDlgItem(m_hWnd, IDC_MESSAGE), SW_SHOW);
+
+ SetDlgItemText(m_hWnd, IDC_MESSAGE, i18n(muT("You can't access the options of HistoryStats as long as the stand-alone configuration dialog of HistoryStats is open. Please close the stand-alone dialog before opening the options dialog of Miranda IM to see the options of HistoryStats here.\r\n\r\nNote that the options offered by both dialogs are the same.")));
+ }
+
+ // reset flag
+ m_bSettingsModified = false;
+}
+
+void DlgOption::onRunStats()
+{
+ if (m_bSettingsModified)
+ {
+ int svar = MessageBox(
+ m_hWnd,
+ i18n(muT("You have unsaved settings. Do you want to save before running HistoryStats?")),
+ i18n(muT("HistoryStats")),
+ MB_YESNOCANCEL);
+
+ if (svar == IDYES)
+ {
+ saveSettings();
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ Statistic::run(*g_pSettings, Statistic::fromOptions, g_hInst, m_hWnd);
+}
+
+void DlgOption::onBandClicked(HANDLE hButton, DWORD dwData)
+{
+ if (dwData >= opFirstPage && dwData <= opLastPage && dwData != m_nCurPage)
+ {
+ if (m_nCurPage != dwData)
+ {
+ if (m_nCurPage != -1)
+ {
+ m_Band.checkButton(m_hPageButton[m_nCurPage], false);
+ m_pPage[m_nCurPage]->hide();
+ }
+
+ m_nCurPage = dwData;
+ }
+
+ m_Band.checkButton(hButton, true);
+ m_pPage[m_nCurPage]->show();
+ }
+ else if (dwData == opCreate || dwData == opCreateWarn)
+ {
+ onRunStats();
+ }
+}
+
+void DlgOption::onBandDropDown(HANDLE hButton, DWORD dwData)
+{
+ if (dwData == opCreate || dwData == opCreateWarn)
+ {
+ // determine position for popup menu
+ RECT rItem = m_Band.getButtonRect(hButton);
+
+ POINT ptMenu = { rItem.right, rItem.bottom };
+
+ ClientToScreen(m_Band, &ptMenu);
+
+ // create and display popup menu
+ HMENU hPopup = CreatePopupMenu();
+
+ if (dwData == opCreateWarn)
+ {
+ AppendMenu(hPopup, MF_STRING, 1, i18n(muT("Show warnings...")));
+ AppendMenu(hPopup, MF_SEPARATOR, 0, NULL);
+ }
+
+ UINT iMenuFlags = g_pSettings->canShowStatistics() ? 0 : (MF_DISABLED | MF_GRAYED);
+
+ AppendMenu(hPopup, iMenuFlags | MF_STRING, 2, i18n(muT("Show statistics")));
+
+ int nShowInfo = TrackPopupMenu(
+ hPopup,
+ TPM_RIGHTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD | TPM_LEFTBUTTON,
+ ptMenu.x,
+ ptMenu.y,
+ 0,
+ getHWnd(),
+ NULL);
+
+ DestroyMenu(hPopup);
+
+ switch (nShowInfo)
+ {
+ case 1:
+ onProblemInfo();
+ break;
+
+ case 2:
+ g_pSettings->showStatistics();
+ break;
+ }
+ }
+}
+
+void DlgOption::onProblemInfo()
+{
+ HelpVec help;
+ bool bAreProblems = reinterpret_cast<SubColumns*>(m_pPage[opColumns])->configHasConflicts(&help);
+
+ if (bAreProblems && help.size() > 0)
+ {
+ ext::string msg = i18n(muT("There are some potential conflicts in your settings. However, this is only a warning and can in general be ignored. The details:"));
+
+
+ citer_each_(HelpVec, i, help)
+ {
+ msg += muT("\r\n\r\n");
+ msg += i->first;
+
+ if (!i->second.empty())
+ {
+ msg += muT("\r\n - ");
+ msg += i->second;
+ }
+ }
+
+ MessageBox(
+ m_hWnd,
+ msg.c_str(),
+ i18n(muT("HistoryStats - Warning")),
+ MB_ICONWARNING | MB_OK);
+ }
+}
+
+void DlgOption::rearrangeControls()
+{
+ RECT rClient, rOther;
+
+ GetClientRect(m_hWnd, &rClient);
+
+ rOther = utils::getWindowRect(m_hWnd, m_Band);
+ rOther.right = rClient.right;
+ utils::moveWindow(m_Band, rOther);
+
+ if (m_bAcquiredLock)
+ {
+ rOther.top = rOther.bottom;
+ rOther.bottom = rClient.bottom;
+
+ array_each_(i, m_pPage)
+ {
+ m_pPage[i]->moveWindow(rOther);
+ }
+ }
+}
+
+void DlgOption::settingsChanged()
+{
+ if (!m_bSettingsModified)
+ {
+ m_bSettingsModified = true;
+ SendMessage(GetParent(m_hWnd), PSM_CHANGED, 0, 0);
+ }
+}
+
+int DlgOption::saveSettings()
+{
+ if (m_bAcquiredLock)
+ {
+ // force all pages to write to local settings store
+ array_each_(i, m_pPage)
+ {
+ m_pPage[i]->saveSettings();
+ }
+
+ // ensure constaints
+ m_LocalS.ensureConstraints();
+
+ // force all pages to read from (possibly corrected) local settings store
+ array_each_(i, m_pPage)
+ {
+ m_pPage[i]->loadSettings();
+ }
+
+ // save to global settings
+ g_pSettings->assignSettings(m_LocalS);
+ g_pSettings->writeToDB();
+
+ AddMainMenu();
+ AddContactMenu();
+ }
+
+ m_bSettingsModified = false;
+
+ return 0;
+}
+
+void DlgOption::updateProblemInfo()
+{
+ bool bShowProblemInfo = reinterpret_cast<SubColumns*>(m_pPage[opColumns])->configHasConflicts(NULL);
+
+ m_Band.showButton(m_hCreateButton, !bShowProblemInfo);
+ m_Band.showButton(m_hCreateWarnButton,bShowProblemInfo);
+}
+
+DlgOption::DlgOption(HWND hWnd)
+ : m_hWnd(hWnd), m_bSettingsModified(false), m_nCurPage(-1), m_LocalS(*g_pSettings),
+ m_hCreateButton(NULL), m_hCreateWarnButton(NULL), m_bAcquiredLock(false)
+{
+}
+
+DlgOption::~DlgOption()
+{
+ // this destructor is called upon WM_DESTROY
+
+ if (m_bAcquiredLock)
+ {
+ // release configure dialog lock
+ m_bAcquiredLock = false;
+ g_bConfigureLock = false;
+
+ // remmber last open page
+ g_pSettings->setLastPage(m_nCurPage);
+
+ // destroy pages
+ array_each_(i, m_pPage)
+ {
+ m_pPage[i]->destroyWindow();
+
+ // MEMO: don't delete them, they will delete themselves in their WM_DESTROY messages
+ m_pPage[i] = NULL;
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/HistoryStats/src/dlgoption.h b/plugins/HistoryStats/src/dlgoption.h
new file mode 100644
index 0000000000..cb51f5d98c
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption.h
@@ -0,0 +1,353 @@
+#if !defined(HISTORYSTATS_GUARD_DLGOPTION_H)
+#define HISTORYSTATS_GUARD_DLGOPTION_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include <map>
+#include <string>
+#include <algorithm>
+
+#include "bandctrl.h"
+#include "optionsctrl.h"
+#include "settings.h"
+#include "resource.h"
+
+class DlgOption
+ : private pattern::NotCopyable<DlgOption>
+{
+private:
+ enum OptionPage {
+ opGlobal = 0,
+ opExclude = 1,
+ opInput = 2,
+ opColumns = 3,
+ opOutput = 4,
+ // ...
+ opFirstPage = 0,
+ opLastPage = 4,
+ // ...
+ opCreate = opLastPage + 1,
+ opCreateWarn = opLastPage + 2,
+ };
+
+ typedef std::pair<ext::string, ext::string> HelpPair;
+ typedef std::vector<HelpPair> HelpVec;
+
+private:
+ class SubBase
+ : private pattern::NotCopyable<SubBase>
+ {
+ private:
+ static INT_PTR CALLBACK staticDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+
+ private:
+ DlgOption* m_pParent;
+ HWND m_hWnd;
+
+ public:
+ SubBase();
+ virtual ~SubBase();
+
+ protected:
+ virtual BOOL handleMsg(UINT msg, WPARAM wParam, LPARAM lParam) = 0;
+ virtual void onWMInitDialog() = 0;
+ virtual void onWMDestroy() { }
+
+ public:
+ virtual void loadSettings() = 0;
+ virtual void saveSettings() = 0;
+
+ protected:
+ HWND getHWnd() { return m_hWnd; }
+ DlgOption* getParent() { return m_pParent; }
+
+ public:
+ void createWindow(DlgOption* pParent, WORD nDlgResource, const RECT& rect);
+ void destroyWindow();
+ void moveWindow(const RECT& rWnd);
+ void show() { ShowWindow(m_hWnd, SW_SHOW); }
+ void hide() { ShowWindow(m_hWnd, SW_HIDE); }
+ };
+
+ class SubGlobal
+ : public SubBase
+ {
+ private:
+ struct SupportInfo {
+ mu_text* szPlugin;
+ mu_text* szTeaser;
+ mu_text* szDescription;
+ mu_text* szLinkTexts;
+ mu_text* szLinkURLs;
+ };
+
+ private:
+ static INT_PTR CALLBACK staticInfoProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+
+ private:
+ OptionsCtrl m_Options;
+
+ OptionsCtrl::Check m_hOnStartup;
+ OptionsCtrl::Check m_hShowMainMenu;
+ OptionsCtrl::Check m_hShowMainMenuSub;
+ OptionsCtrl::Check m_hShowContactMenu;
+ OptionsCtrl::Check m_hShowContactMenuPseudo;
+ OptionsCtrl::Group m_hProtocols;
+ std::vector<OptionsCtrl::Check> m_hHideContactMenuProtos;
+ OptionsCtrl::Radio m_hGraphicsMode;
+ OptionsCtrl::Radio m_hGraphicsModePNG;
+ OptionsCtrl::Radio m_hPNGMode;
+ OptionsCtrl::Check m_hThreadLowPriority;
+ OptionsCtrl::Edit m_hPathToBrowser;
+
+ bool m_bShowInfo;
+ int m_nInfoHeight;
+
+ public:
+ SubGlobal();
+ virtual ~SubGlobal();
+
+ protected:
+ virtual BOOL handleMsg(UINT msg, WPARAM wParam, LPARAM lParam);
+ virtual void onWMInitDialog();
+
+ public:
+ virtual void loadSettings();
+ virtual void saveSettings();
+
+ private:
+ void initSupportInfo();
+ void rearrangeControls();
+ void toggleInfo();
+ void onShowSupportInfo(const SupportInfo& info);
+
+ public:
+ bool isPNGOutput() { return (m_Options.getRadioChecked(m_hGraphicsMode) == Settings::gmPNG); }
+ int getPNGMode() { return m_Options.getRadioChecked(m_hPNGMode); }
+ };
+
+ class SubExclude
+ : public SubBase
+ {
+ private:
+ HANDLE m_hItemAll;
+ bool m_bChanged;
+
+ public:
+ SubExclude();
+ virtual ~SubExclude();
+
+ protected:
+ virtual BOOL handleMsg(UINT msg, WPARAM wParam, LPARAM lParam);
+ virtual void onWMInitDialog();
+
+ private:
+ static void staticRecreateIcons(LPARAM lParam);
+
+ public:
+ virtual void loadSettings();
+ virtual void saveSettings();
+
+ private:
+ void customizeList(HWND hCList);
+ void updateAllGroups(HWND hCList, HANDLE hFirstItem, HANDLE hParentItem);
+ void updateAllContacts(HWND hCList);
+ void setAll(HWND hCList, HANDLE hFirstItem, int iImage, bool bIterate);
+ };
+
+ class SubInput
+ : public SubBase
+ {
+ private:
+ OptionsCtrl m_Options;
+
+ OptionsCtrl::Edit m_hChatSessionMinDur;
+ OptionsCtrl::Edit m_hChatSessionTimeout;
+ OptionsCtrl::Edit m_hAverageMinTime;
+ OptionsCtrl::Edit m_hWordDelimiters;
+ OptionsCtrl::Group m_hProtocols;
+ std::vector<OptionsCtrl::Check> m_hProtosIgnore;
+ OptionsCtrl::Radio m_hMetaContactsMode;
+ OptionsCtrl::Check m_hMergeContacts;
+ OptionsCtrl::Check m_hMergeContactsGroups;
+ OptionsCtrl::Radio m_hMergeMode;
+ OptionsCtrl::Edit m_hIgnoreOlder;
+ OptionsCtrl::Edit m_hIgnoreBefore;
+ OptionsCtrl::Edit m_hIgnoreAfter;
+ OptionsCtrl::Check m_hFilterRawRTF;
+ OptionsCtrl::Check m_hFilterBBCodes;
+
+ public:
+ SubInput();
+ virtual ~SubInput();
+
+ protected:
+ virtual BOOL handleMsg(UINT msg, WPARAM wParam, LPARAM lParam);
+ virtual void onWMInitDialog();
+
+ public:
+ virtual void loadSettings();
+ virtual void saveSettings();
+ };
+
+ class SubColumns
+ : public SubBase
+ {
+ private:
+ enum ColumnAction {
+ caAdd = 0,
+ caDel = 1,
+ caMoveDown = 2,
+ caMoveUp = 3,
+ // ...
+ caFirstAction = 0,
+ caLastAction = 3,
+ };
+
+ private:
+ static INT_PTR CALLBACK staticAddProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+
+ private:
+ BandCtrl m_Band;
+
+ HANDLE m_hActionButtons[4];
+
+ OptionsCtrl m_Columns;
+ OptionsCtrl m_Options;
+
+ OptionsCtrl::Edit m_hColTitle;
+
+ bool m_bShowInfo;
+ int m_nInfoHeight;
+
+ public:
+ SubColumns();
+ virtual ~SubColumns();
+
+ protected:
+ virtual BOOL handleMsg(UINT msg, WPARAM wParam, LPARAM lParam);
+ virtual void onWMInitDialog();
+ virtual void onWMDestroy();
+
+ public:
+ virtual void loadSettings();
+ virtual void saveSettings();
+
+ private:
+ void rearrangeControls();
+ void toggleInfo();
+ void addCol(int nCol);
+ void onColSelChanging(HANDLE hItem, DWORD dwData);
+ void onColSelChanged(HANDLE hItem, DWORD dwData);
+ void onColItemDropped(HANDLE hItem, HANDLE hDropTarget, BOOL bAbove);
+ void onBandClicked(HANDLE hButton, DWORD dwData);
+ void onBandDropDown(HANDLE hButton, DWORD dwData);
+ void onAdd();
+ void onDel();
+ void onMoveUp();
+ void onMoveDown();
+ void onColumnButton(HANDLE hButton, DWORD dwData);
+
+ public:
+ bool configHasConflicts(HelpVec* pHelp);
+ };
+
+ class SubOutput
+ : public SubBase
+ {
+ private:
+ OptionsCtrl m_Options;
+
+ OptionsCtrl::Check m_hRemoveEmptyContacts;
+ OptionsCtrl::Check m_hRemoveInChatsZero;
+ OptionsCtrl::Check m_hRemoveInBytesZero;
+ OptionsCtrl::Check m_hRemoveOutChatsZero;
+ OptionsCtrl::Check m_hRemoveOutBytesZero;
+ OptionsCtrl::Check m_hOmitContacts;
+ OptionsCtrl::Check m_hOmitByValue;
+ OptionsCtrl::Combo m_hOmitByValueData;
+ OptionsCtrl::Edit m_hOmitByValueLimit;
+ OptionsCtrl::Check m_hOmitByTime;
+ OptionsCtrl::Edit m_hOmitByTimeDays;
+ OptionsCtrl::Check m_hOmitByRank;
+ OptionsCtrl::Edit m_hOmitNumOnTop;
+ OptionsCtrl::Check m_hOmittedInTotals;
+ OptionsCtrl::Check m_hOmittedInExtraRow;
+ OptionsCtrl::Check m_hCalcTotals;
+ OptionsCtrl::Check m_hTableHeader;
+ OptionsCtrl::Edit m_hTableHeaderRepeat;
+ OptionsCtrl::Check m_hTableHeaderVerbose;
+ OptionsCtrl::Check m_hHeaderTooltips;
+ OptionsCtrl::Check m_hHeaderTooltipsIfCustom;
+ OptionsCtrl::Group m_hSort;
+ OptionsCtrl::Combo m_hSortBy[Settings::cNumSortLevels];
+ OptionsCtrl::Radio m_hSortDir[Settings::cNumSortLevels];
+ OptionsCtrl::Edit m_hNick;
+ OptionsCtrl::Check m_hOutputVariables;
+ OptionsCtrl::Edit m_hOutputFile;
+ OptionsCtrl::Check m_hOutputExtraToFolder;
+ OptionsCtrl::Edit m_hOutputExtraFolder;
+ OptionsCtrl::Check m_hOverwriteAlways;
+ OptionsCtrl::Check m_hAutoOpenOptions;
+ OptionsCtrl::Check m_hAutoOpenStartup;
+ OptionsCtrl::Check m_hAutoOpenMenu;
+
+ std::vector<int> m_SortKeyToIndex;
+ std::vector<int> m_IndexToSortKey;
+
+ public:
+ SubOutput();
+ virtual ~SubOutput();
+
+ protected:
+ virtual BOOL handleMsg(UINT msg, WPARAM wParam, LPARAM lParam);
+ virtual void onWMInitDialog();
+
+ public:
+ virtual void loadSettings();
+ virtual void saveSettings();
+
+ private:
+ void onChanged(HANDLE hItem);
+ };
+
+public:
+ static INT_PTR CALLBACK staticDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+
+private:
+ HWND m_hWnd;
+ bool m_bSettingsModified;
+ int m_nCurPage;
+ BandCtrl m_Band;
+ HANDLE m_hPageButton[opLastPage + 1];
+ SubBase* m_pPage[opLastPage + 1];
+ HANDLE m_hCreateButton;
+ HANDLE m_hCreateWarnButton;
+ bool m_bAcquiredLock;
+
+ Settings m_LocalS;
+
+private:
+ void onWMInitDialog();
+ void onRunStats();
+ void onBandClicked(HANDLE hButton, DWORD dwData);
+ void onBandDropDown(HANDLE hButton, DWORD dwData);
+ void onProblemInfo();
+ void rearrangeControls();
+
+private:
+ Settings& getLocalSettings() { return m_LocalS; }
+ HWND getHWnd() { return m_hWnd; }
+ void settingsChanged();
+ bool isPNGOutput() { return m_pPage[opGlobal] ? reinterpret_cast<SubGlobal*>(m_pPage[opGlobal])->isPNGOutput() : false; }
+ int getPNGMode() { return m_pPage[opGlobal] ? reinterpret_cast<SubGlobal*>(m_pPage[opGlobal])->getPNGMode() : 0; }
+ void updateProblemInfo();
+ int saveSettings();
+
+public:
+ explicit DlgOption(HWND hWnd);
+ ~DlgOption();
+};
+
+#endif // HISTORYSTATS_GUARD_DLGOPTION_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/dlgoption_subbase.cpp b/plugins/HistoryStats/src/dlgoption_subbase.cpp
new file mode 100644
index 0000000000..921195d4c9
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption_subbase.cpp
@@ -0,0 +1,76 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+#include "main.h"
+
+/*
+ * DlgOption::SubBase
+ */
+
+INT_PTR CALLBACK DlgOption::SubBase::staticDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ SubBase* pDlg = reinterpret_cast<SubBase*>(GetWindowLong(hDlg, DWLP_USER));
+
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ pDlg = reinterpret_cast<SubBase*>(lParam);
+ SetWindowLong(hDlg, DWLP_USER, reinterpret_cast<LONG>(pDlg));
+ pDlg->m_hWnd = hDlg;
+ pDlg->onWMInitDialog();
+ pDlg->loadSettings();
+ return TRUE;
+
+ case WM_DESTROY:
+ pDlg->onWMDestroy();
+ delete pDlg;
+ SetWindowLong(hDlg, DWLP_USER, 0);
+ return TRUE;
+ }
+
+ return pDlg ? pDlg->handleMsg(msg, wParam, lParam) : FALSE;
+}
+
+DlgOption::SubBase::SubBase()
+ : m_pParent(NULL), m_hWnd(NULL)
+{
+}
+
+DlgOption::SubBase::~SubBase()
+{
+}
+
+void DlgOption::SubBase::createWindow(DlgOption* pParent, WORD nDlgResource, const RECT& rect)
+{
+ m_pParent = pParent;
+
+ CreateDialogParam(
+ g_hInst,
+ MAKEINTRESOURCE(nDlgResource),
+ m_pParent->getHWnd(),
+ staticDlgProc,
+ reinterpret_cast<LPARAM>(this));
+
+ MoveWindow(
+ m_hWnd,
+ rect.left,
+ rect.top,
+ rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+}
+
+void DlgOption::SubBase::destroyWindow()
+{
+ if (m_hWnd)
+ {
+ DestroyWindow(m_hWnd);
+
+ // exit NOW since we destroyed ourselves
+ return;
+ }
+}
+
+void DlgOption::SubBase::moveWindow(const RECT& rWnd)
+{
+ utils::moveWindow(m_hWnd, rWnd);
+}
diff --git a/plugins/HistoryStats/src/dlgoption_subcolumns.cpp b/plugins/HistoryStats/src/dlgoption_subcolumns.cpp
new file mode 100644
index 0000000000..52155bd0f0
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption_subcolumns.cpp
@@ -0,0 +1,832 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+#include "column.h"
+#include "bandctrl.h"
+#include "main.h"
+
+/*
+ * DlgOption::SubColumns
+ */
+
+INT_PTR CALLBACK DlgOption::SubColumns::staticAddProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ {
+ TranslateDialogDefault(hDlg);
+
+ SendMessage(hDlg, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_HISTORYSTATS))));
+
+ utils::centerDialog(hDlg);
+
+ HWND hWndList = GetDlgItem(hDlg, IDC_COLUMN);
+
+ upto_each_(i, Column::countColInfo())
+ {
+ int nIndex = SendMessage(hWndList, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(Column::getColInfo(i).m_Title));
+ SendMessage(hWndList, LB_SETITEMDATA, nIndex, static_cast<LPARAM>(i));
+ }
+
+ if (Column::countColInfo() > 0)
+ {
+ SendMessage(hWndList, LB_SETCURSEL, 0, 0);
+
+ SetDlgItemText(hDlg, IDC_DESCRIPTION, Column::getColInfo(0).m_Description);
+ }
+ }
+ return TRUE;
+
+ case WM_COMMAND:
+ switch (LOWORD(wParam))
+ {
+ case IDOK:
+ {
+ HWND hWndList = GetDlgItem(hDlg, IDC_COLUMN);
+
+ int nIndex = SendMessage(hWndList, LB_GETCURSEL, 0, 0);
+
+ if (nIndex != LB_ERR)
+ {
+ int nData = SendMessage(hWndList, LB_GETITEMDATA, nIndex, 0);
+
+ EndDialog(hDlg, nIndex);
+ }
+ else
+ {
+ EndDialog(hDlg, -1);
+ }
+ }
+ return TRUE;
+
+ case IDCANCEL:
+ EndDialog(hDlg, -1);
+ return TRUE;
+
+ case IDC_COLUMN:
+ {
+ if (HIWORD(wParam) == LBN_SELCHANGE)
+ {
+ HWND hWndList = GetDlgItem(hDlg, IDC_COLUMN);
+
+ int nIndex = SendMessage(hWndList, LB_GETCURSEL, 0, 0);
+
+ if (nIndex != LB_ERR)
+ {
+ int nData = SendMessage(hWndList, LB_GETITEMDATA, nIndex, 0);
+
+ SetDlgItemText(hDlg, IDC_DESCRIPTION, Column::getColInfo(nData).m_Description);
+ }
+ else
+ {
+ SetDlgItemText(hDlg, IDC_DESCRIPTION, muT(""));
+ }
+ }
+ else if (HIWORD(wParam) == LBN_DBLCLK)
+ {
+ SendMessage(hDlg, WM_COMMAND, MAKEWPARAM(IDOK, 0), NULL);
+ }
+ }
+ break;
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+DlgOption::SubColumns::SubColumns()
+ : m_hColTitle(NULL),
+ m_bShowInfo(true),
+ m_nInfoHeight(0)
+{
+}
+
+DlgOption::SubColumns::~SubColumns()
+{
+ g_pSettings->setShowColumnInfo(m_bShowInfo);
+}
+
+BOOL DlgOption::SubColumns::handleMsg(UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_WINDOWPOSCHANGED:
+ rearrangeControls();
+ return TRUE;
+
+ case WM_COMMAND:
+ if (LOWORD(wParam) == IDC_INFOLABEL && HIWORD(wParam) == STN_CLICKED)
+ {
+ m_bShowInfo = !m_bShowInfo;
+ toggleInfo();
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+ switch (reinterpret_cast<NMHDR*>(lParam)->idFrom)
+ {
+ case IDC_BAND:
+ {
+ BandCtrl::NMBANDCTRL* pNM = reinterpret_cast<BandCtrl::NMBANDCTRL*>(lParam);
+
+ if (pNM->hdr.code == BandCtrl::BCN_CLICKED)
+ {
+ onBandClicked(pNM->hButton, pNM->dwData);
+ }
+ else if (pNM->hdr.code == BandCtrl::BCN_DROPDOWN)
+ {
+ onBandDropDown(pNM->hButton, pNM->dwData);
+ }
+ }
+ break;
+
+ case IDC_COLUMNS:
+ {
+ OptionsCtrl::NMOPTIONSCTRL* pNM = reinterpret_cast<OptionsCtrl::NMOPTIONSCTRL*>(lParam);
+
+ if (pNM->hdr.code == OptionsCtrl::OCN_MODIFIED)
+ {
+ getParent()->settingsChanged();
+ getParent()->updateProblemInfo();
+ }
+ else if (pNM->hdr.code == OptionsCtrl::OCN_SELCHANGED)
+ {
+ onColSelChanged(pNM->hItem, pNM->dwData);
+ }
+ else if (pNM->hdr.code == OptionsCtrl::OCN_SELCHANGING)
+ {
+ onColSelChanging(pNM->hItem, pNM->dwData);
+ }
+ else if (pNM->hdr.code == OptionsCtrl::OCN_ITEMDROPPED)
+ {
+ OptionsCtrl::NMOPTIONSCTRLDROP* pNM2 = reinterpret_cast<OptionsCtrl::NMOPTIONSCTRLDROP*>(pNM);
+
+ onColItemDropped(pNM2->hItem, pNM2->hDropTarget, pNM2->bAbove);
+ }
+ }
+ break;
+
+ case IDC_OPTIONS:
+ {
+ OptionsCtrl::NMOPTIONSCTRL * pNM = reinterpret_cast<OptionsCtrl::NMOPTIONSCTRL*>(lParam);
+
+ if (pNM->hdr.code == OptionsCtrl::OCN_MODIFIED)
+ {
+ getParent()->settingsChanged();
+ }
+ else if (pNM->hdr.code == OptionsCtrl::OCN_CLICKED)
+ {
+ onColumnButton(pNM->hItem, pNM->dwData);
+ }
+ }
+ break;
+
+ case IDC_INFO:
+ {
+ NMTREEVIEW* pNM = reinterpret_cast<NMTREEVIEW*>(lParam);
+
+ if (pNM->hdr.code == TVN_ITEMEXPANDING)
+ {
+ if (pNM->action == TVE_COLLAPSE || pNM->action == TVE_COLLAPSERESET ||
+ (pNM->action == TVE_TOGGLE && pNM->itemNew.state & TVIS_EXPANDED))
+ {
+ SetWindowLong(getHWnd(), DWLP_MSGRESULT, TRUE);
+ return TRUE;
+ }
+ }
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgOption::SubColumns::onWMInitDialog()
+{
+ TranslateDialogDefault(getHWnd());
+
+ // init column band
+ m_Band << GetDlgItem(getHWnd(), IDC_BAND);
+
+ static const struct {
+ WORD iconId;
+ mu_text* szTooltip;
+ bool bRight;
+ bool bDropDown;
+ bool bDisabled;
+ } columnBand[] = {
+ { IDI_COL_ADD , I18N(muT("Add column...")), false, true , false },
+ { IDI_COL_DEL , I18N(muT("Delete column")), false, false, true },
+ { IDI_COL_DOWN, I18N(muT("Move down")) , true , false, true },
+ { IDI_COL_UP , I18N(muT("Move up")) , true , false, true },
+ };
+
+ array_each_(i, columnBand)
+ {
+ HICON hIcon = reinterpret_cast<HICON>(LoadImage(g_hInst, MAKEINTRESOURCE(columnBand[i].iconId), IMAGE_ICON, OS::smIconCX(), OS::smIconCY(), 0));
+ DWORD dwFlags =
+ (columnBand[i].bRight ? BandCtrl::BCF_RIGHT : 0) |
+ (columnBand[i].bDropDown ? BandCtrl::BCF_DROPDOWN : 0) |
+ (columnBand[i].bDisabled ? BandCtrl::BCF_DISABLED : 0);
+
+ m_hActionButtons[i] = m_Band.addButton(dwFlags, hIcon, i, i18n(columnBand[i].szTooltip));
+
+ DestroyIcon(hIcon);
+ }
+
+ // init options controls
+ m_Columns << GetDlgItem(getHWnd(), IDC_COLUMNS);
+
+ // init option tree(s)
+ m_Options << GetDlgItem(getHWnd(), IDC_OPTIONS);
+
+ // int column info
+ HWND hInfo = GetDlgItem(getHWnd(), IDC_INFO);
+
+ SetWindowLong(hInfo, GWL_STYLE, GetWindowLong(hInfo, GWL_STYLE) | TVS_NOHSCROLL);
+
+ m_bShowInfo = g_pSettings->getShowColumnInfo();
+ toggleInfo();
+}
+
+void DlgOption::SubColumns::onWMDestroy()
+{
+ // avoid OCN_SELCHANGING messages when options are already destroyed
+ m_Columns.deleteAllItems();
+}
+
+void DlgOption::SubColumns::loadSettings()
+{
+ // remeber currently selected item
+ HANDLE hSelItem = m_Columns.getSelection();
+ Column* pPrevCol = NULL;
+
+ if (hSelItem)
+ {
+ pPrevCol = reinterpret_cast<Column*>(m_Columns.getItemData(hSelItem));
+
+ hSelItem = NULL;
+ }
+
+ // remember scroll bar position
+ int nScrollPosV = m_Columns.getScrollPos(SB_VERT);
+
+ // refill column tree
+ m_Columns.setRedraw(false);
+ m_Columns.deleteAllItems();
+
+ Settings& localS = getParent()->getLocalSettings();
+
+ upto_each_(j, localS.countCol())
+ {
+ Column* pCol = localS.getCol(j);
+ ext::string colTitle = pCol->getTitleForOptions();
+ OptionsCtrl::Check hColCheck = m_Columns.insertCheck(NULL, colTitle.c_str(), 0, reinterpret_cast<DWORD>(pCol));
+
+ m_Columns.checkItem(hColCheck, pCol->isEnabled());
+
+ if (!hSelItem && pCol == pPrevCol)
+ {
+ hSelItem = hColCheck;
+ }
+ }
+
+ m_Columns.ensureVisible(NULL);
+ m_Columns.setRedraw(true);
+
+ // restore scroll bar position
+ m_Columns.setScrollPos(SB_VERT, nScrollPosV);
+
+ // restore selected item
+ m_Columns.selectItem(hSelItem);
+}
+
+void DlgOption::SubColumns::saveSettings()
+{
+ HANDLE hSelItem = m_Columns.getSelection();
+
+ if (hSelItem)
+ {
+ // deselect and select current item to save its options to localS
+ m_Columns.selectItem(NULL);
+ m_Columns.selectItem(hSelItem);
+ }
+
+ HANDLE hItem = m_Columns.getFirstItem();
+
+ while (hItem)
+ {
+ Column* pCol = reinterpret_cast<Column*>(m_Columns.getItemData(hItem));
+
+ if (pCol)
+ {
+ pCol->setEnabled(m_Columns.isItemChecked(hItem));
+ }
+
+ hItem = m_Columns.getNextItem(hItem);
+ }
+}
+
+void DlgOption::SubColumns::rearrangeControls()
+{
+ RECT rClient, rWnd;
+ int offsetY;
+
+ if (m_nInfoHeight == 0)
+ {
+ m_nInfoHeight = utils::getWindowRect(getHWnd(), IDC_INFO).bottom;
+ m_nInfoHeight -= utils::getWindowRect(getHWnd(), IDC_INFOLABEL).bottom;
+ }
+
+ GetClientRect(getHWnd(), &rClient);
+
+ // columns tree
+ rWnd = utils::getWindowRect(getHWnd(), m_Columns);
+ rWnd.bottom = rClient.bottom;
+ utils::moveWindow(m_Columns, rWnd);
+
+ // column info list
+ rWnd = utils::getWindowRect(getHWnd(), IDC_INFO);
+ offsetY = rClient.bottom + (m_bShowInfo ? 0 : m_nInfoHeight) - rWnd.bottom;
+ OffsetRect(&rWnd, 0, offsetY);
+ rWnd.right = rClient.right;
+ utils::moveWindow(getHWnd(), IDC_INFO, rWnd);
+
+ // column info list label
+ rWnd = utils::getWindowRect(getHWnd(), IDC_INFOLABEL);
+ OffsetRect(&rWnd, 0, offsetY);
+ rWnd.right = rClient.right;
+ utils::moveWindow(getHWnd(), IDC_INFOLABEL, rWnd);
+
+ // options tree
+ rWnd = utils::getWindowRect(getHWnd(), m_Options);
+ rWnd.right = rClient.right;
+ rWnd.bottom += offsetY;
+ utils::moveWindow(m_Options, rWnd);
+}
+
+void DlgOption::SubColumns::toggleInfo()
+{
+ HWND hInfo = GetDlgItem(getHWnd(), IDC_INFO);
+ const mu_text* szInfoLabelText = m_bShowInfo ? I18N(muT("Hide additional column info...")) : I18N(muT("Show additional column info..."));
+
+ SetDlgItemText(getHWnd(), IDC_INFOLABEL, i18n(szInfoLabelText));
+ ShowWindow(hInfo, m_bShowInfo ? SW_SHOW : SW_HIDE);
+ EnableWindow(hInfo, BOOL_(m_bShowInfo));
+
+ rearrangeControls();
+}
+
+void DlgOption::SubColumns::addCol(int nCol)
+{
+ if (nCol != -1)
+ {
+ Column* pCol = Column::fromUID(Column::getColInfo(nCol).m_UID);
+
+ getParent()->getLocalSettings().addCol(pCol);
+
+ OptionsCtrl::Check hColCheck = m_Columns.insertCheck(NULL, pCol->getTitle(), 0, reinterpret_cast<DWORD>(pCol));
+
+ m_Columns.checkItem(hColCheck, pCol->isEnabled());
+
+ getParent()->settingsChanged();
+ }
+}
+
+void DlgOption::SubColumns::onColSelChanging(HANDLE hItem, DWORD dwData)
+{
+ if (hItem)
+ {
+ Column* pCol = reinterpret_cast<Column*>(m_Columns.getItemData(hItem));
+
+ // general column settings
+ if (m_hColTitle)
+ {
+ pCol->setCustomTitle(m_Options.getEditString(m_hColTitle));
+
+ // adjust title in column tree
+ ext::string colTitle = pCol->getTitleForOptions();
+
+ m_Columns.setItemLabel(hItem, colTitle.c_str());
+
+ m_hColTitle = NULL;
+ }
+
+ // column specific settings
+ pCol->configFromUI(m_Options);
+
+ // check for conflicts: PNG output vs. column options
+ getParent()->updateProblemInfo();
+ }
+
+ // clear column settings
+ m_Options.setRedraw(false);
+ m_Options.deleteAllItems();
+ m_Options.setRedraw(true);
+
+ // clear output info
+ HWND hInfo = GetDlgItem(getHWnd(), IDC_INFO);
+ TreeView_DeleteAllItems(hInfo);
+}
+
+void DlgOption::SubColumns::onColSelChanged(HANDLE hItem, DWORD dwData)
+{
+ m_Options.setRedraw(false);
+ m_Options.deleteAllItems();
+
+ if (hItem)
+ {
+ Column* pCol = reinterpret_cast<Column*>(m_Columns.getItemData(hItem));
+
+ // general column settings
+ OptionsCtrl::Group hGeneral = m_Options.insertGroup(NULL, i18n(muT("General column settings")), OptionsCtrl::OCF_ROOTGROUP);
+
+ m_hColTitle = m_Options.insertEdit(hGeneral, i18n(muT("Title (default if empty)")), pCol->getCustomTitle().c_str());
+
+ // column specific settings
+ if (pCol->getFeatures() & Column::cfHasConfig)
+ {
+ OptionsCtrl::Group hSpecific = m_Options.insertGroup(NULL, i18n(muT("Column specific settings")), OptionsCtrl::OCF_ROOTGROUP);
+
+ pCol->configToUI(m_Options, hSpecific);
+ }
+
+ m_Options.ensureVisible(NULL);
+
+ // show output info for current column
+ HWND hInfo = GetDlgItem(getHWnd(), IDC_INFO);
+ TVINSERTSTRUCT tvi;
+ bool bPNGOutput = getParent()->isPNGOutput();
+ int nPNGMode = getParent()->getPNGMode();
+ int restrictions = pCol->configGetRestrictions(NULL);
+
+ SendMessage(hInfo, WM_SETREDRAW, FALSE, 0);
+ TreeView_DeleteAllItems(hInfo);
+
+ tvi.hParent = TVI_ROOT;
+ tvi.hInsertAfter = TVI_LAST;
+ tvi.item.mask = TVIF_TEXT | TVIF_STATE;
+ tvi.item.state = TVIS_EXPANDED;
+ tvi.item.stateMask = TVIS_EXPANDED;
+
+ tvi.item.pszText = const_cast<mu_text*>(i18n(muT("For this config the selected column...")));
+ tvi.hParent = TreeView_InsertItem(hInfo, &tvi);
+
+ // show capabilities of column
+ ext::string msg = i18n(muT("...can be output as: "));
+
+ if (restrictions & Column::crHTMLMask)
+ {
+ // MEMO: don't distinguish between full/partial since not yet supported
+ msg += i18n(muT("HTML"));
+ }
+
+ if (restrictions & Column::crPNGMask)
+ {
+ if (restrictions & Column::crHTMLMask)
+ {
+ msg += muT(", ");
+ }
+
+ msg += ((restrictions & Column::crPNGMask) == Column::crPNGPartial) ? i18n(muT("PNG (partial)")) : i18n(muT("PNG"));
+ }
+
+ tvi.item.pszText = const_cast<mu_text*>(msg.c_str());
+ TreeView_InsertItem(hInfo, &tvi);
+
+ // show effect of current config
+ msg = i18n(muT("...will be output as: "));
+
+ /*
+ * the logic (output mode -> restrictions -> effect):
+ *
+ * HTML -> HTML-full -> HTML
+ * -> !HTML-full -> nothing (column will be ignored)
+ *
+ * PNG-fallback -> HTML-full -> HTML
+ * -> HTML-full | PNG-partial -> HTML as fallback
+ * -> (HTML-full |) PNG-full -> PNG
+ *
+ * PNG-enforce -> HTML-full -> HTML
+ * -> HTML-full | PNG-partial -> PNG, ignoring some settings
+ * -> (HTML-full |) PNG-full -> PNG
+ *
+ * PNG-preferHTML -> HTML-full (| PNG-...) -> HTML
+ * -> PNG-full -> PNG
+ */
+
+ if (!bPNGOutput)
+ {
+ msg += ((restrictions & Column::crHTMLMask) == Column::crHTMLFull) ? i18n(muT("HTML")) : i18n(muT("Nothing (column will be skipped)"));
+ }
+ else if (nPNGMode != Settings::pmPreferHTML) // && bPNGOutput
+ {
+ if (restrictions == (Column::crHTMLFull | Column::crPNGPartial))
+ {
+ msg += (nPNGMode == Settings::pmHTMLFallBack) ? i18n(muT("HTML as fallback")) : i18n(muT("PNG, ignoring some sttings"));
+ }
+ else // !(html-full | png-partial)
+ {
+ msg += ((restrictions & Column::crPNGMask) == Column::crPNGFull) ? i18n(muT("PNG")) : i18n(muT("HTML"));
+ }
+ }
+ else // bPNGOutput && nPNGMode == Settings::pmPreferHTML
+ {
+ msg += ((restrictions & Column::crHTMLMask) == Column::crHTMLFull) ? i18n(muT("HTML")) : i18n(muT("PNG"));
+ }
+
+ tvi.item.pszText = const_cast<mu_text*>(msg.c_str());
+ TreeView_InsertItem(hInfo, &tvi);
+
+ SendMessage(hInfo, WM_SETREDRAW, TRUE, 0);
+ }
+
+ // (de)activate band controls
+ m_Band.enableButton(m_hActionButtons[caDel], bool_(hItem));
+ m_Band.enableButton(m_hActionButtons[caMoveUp], hItem && m_Columns.getPrevItem(hItem));
+ m_Band.enableButton(m_hActionButtons[caMoveDown], hItem && m_Columns.getNextItem(hItem));
+
+ m_Options.setRedraw(true);
+}
+
+void DlgOption::SubColumns::onColItemDropped(HANDLE hItem, HANDLE hDropTarget, BOOL bAbove)
+{
+ // check if dropped before or after hItem and abort
+ if (hItem == hDropTarget)
+ {
+ return;
+ }
+
+ // convert dropped below to dropped above
+ if (!bAbove)
+ {
+ hDropTarget = m_Columns.getPrevItem(hDropTarget);
+ }
+
+ // check if dropped before or after hItem and abort (part 2)
+ if (hItem == hDropTarget || (hDropTarget && m_Columns.getNextItem(hDropTarget) == hItem))
+ {
+ return;
+ }
+
+ // perform move
+ Column* pItem = reinterpret_cast<Column*>(m_Columns.getItemData(hItem));
+ Column* pDropTaregt = hDropTarget ? reinterpret_cast<Column*>(m_Columns.getItemData(hDropTarget)) : NULL;
+
+ m_Columns.selectItem(NULL);
+
+ getParent()->getLocalSettings().moveCol(pItem, pDropTaregt);
+ m_Columns.moveItem(hItem, hDropTarget);
+
+ m_Columns.selectItem(hItem);
+
+ getParent()->settingsChanged();
+}
+
+void DlgOption::SubColumns::onBandClicked(HANDLE hButton, DWORD dwData)
+{
+ switch (dwData)
+ {
+ case caAdd:
+ onAdd();
+ break;
+
+ case caDel:
+ onDel();
+ break;
+
+ case caMoveUp:
+ onMoveUp();
+ break;
+
+ case caMoveDown:
+ onMoveDown();
+ break;
+ }
+}
+
+void DlgOption::SubColumns::onBandDropDown(HANDLE hButton, DWORD dwData)
+{
+ if (dwData == caAdd)
+ {
+ // determine position for popup menu
+ RECT rItem = m_Band.getButtonRect(hButton);
+ POINT ptMenu = { rItem.left, rItem.bottom };
+
+ ClientToScreen(m_Band, &ptMenu);
+
+ // create and display popup menu
+ HMENU hPopup = CreatePopupMenu();
+
+ upto_each_(i, Column::countColInfo())
+ {
+ AppendMenu(hPopup, MF_STRING, i + 1, Column::getColInfo(i).m_Title);
+ }
+
+ int nCol = -1 + TrackPopupMenu(
+ hPopup,
+ TPM_LEFTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD | TPM_LEFTBUTTON,
+ ptMenu.x,
+ ptMenu.y,
+ 0,
+ getHWnd(),
+ NULL);
+
+ DestroyMenu(hPopup);
+
+ addCol(nCol);
+ }
+}
+
+void DlgOption::SubColumns::onAdd()
+{
+ int nCol = DialogBox(g_hInst, MAKEINTRESOURCE(IDD_COLADD), getHWnd(), staticAddProc);
+
+ addCol(nCol);
+}
+
+void DlgOption::SubColumns::onDel()
+{
+ HANDLE hSelItem = m_Columns.getSelection();
+
+ if (hSelItem)
+ {
+ Column* pCol = reinterpret_cast<Column*>(m_Columns.getItemData(hSelItem));
+
+ if (pCol)
+ {
+ getParent()->getLocalSettings().delCol(pCol);
+ m_Columns.deleteItem(hSelItem);
+
+ getParent()->settingsChanged();
+ }
+ }
+}
+
+void DlgOption::SubColumns::onMoveUp()
+{
+ HANDLE hSel, hPrev2;
+
+ if (!(hSel = m_Columns.getSelection()))
+ {
+ return;
+ }
+
+ if (!(hPrev2 = m_Columns.getPrevItem(hSel)))
+ {
+ return;
+ }
+
+ hPrev2 = m_Columns.getPrevItem(hPrev2);
+
+ Column* pSelCol = reinterpret_cast<Column*>(m_Columns.getItemData(hSel));
+ Column* pPrev2Col = hPrev2 ? reinterpret_cast<Column*>(m_Columns.getItemData(hPrev2)) : NULL;
+
+ m_Columns.selectItem(NULL);
+
+ getParent()->getLocalSettings().moveCol(pSelCol, pPrev2Col);
+ m_Columns.moveItem(hSel, hPrev2);
+
+ m_Columns.selectItem(hSel);
+
+ getParent()->settingsChanged();
+}
+
+void DlgOption::SubColumns::onMoveDown()
+{
+ HANDLE hSel, hNext;
+
+ if (!(hSel = m_Columns.getSelection()))
+ {
+ return;
+ }
+
+ if (!(hNext = m_Columns.getNextItem(hSel)))
+ {
+ return;
+ }
+
+ Column* pSelCol = reinterpret_cast<Column*>(m_Columns.getItemData(hSel));
+ Column* pNextCol = reinterpret_cast<Column*>(m_Columns.getItemData(hNext));
+
+ m_Columns.selectItem(NULL);
+
+ getParent()->getLocalSettings().moveCol(pSelCol, pNextCol);
+ m_Columns.moveItem(hSel, hNext);
+
+ m_Columns.selectItem(hSel);
+
+ getParent()->settingsChanged();
+}
+
+void DlgOption::SubColumns::onColumnButton(HANDLE hButton, DWORD dwData)
+{
+ if (dwData == Settings::biFilterWords)
+ {
+ HANDLE hSel = m_Columns.getSelection();
+ Column* pCol = reinterpret_cast<Column*>(m_Columns.getItemData(hSel));
+
+ if (hSel && pCol)
+ {
+ if (getParent()->getLocalSettings().manageFilterWords(getHWnd(), pCol))
+ {
+ getParent()->settingsChanged();
+ }
+ }
+ }
+}
+
+bool DlgOption::SubColumns::configHasConflicts(HelpVec* pHelp)
+{
+ bool bPNGOutput = getParent()->isPNGOutput();
+ int nPNGMode = getParent()->getPNGMode();
+ HANDLE hItem = m_Columns.getFirstItem();
+
+ int nConflicts = 0;
+ ext::string curDetails;
+
+ while (hItem)
+ {
+ if (m_Columns.isItemChecked(hItem))
+ {
+ Column* pCol = reinterpret_cast<Column*>(m_Columns.getItemData(hItem));
+
+ if (pCol)
+ {
+ int restrictions = pCol->configGetRestrictions(pHelp ? &curDetails : NULL);
+
+ // sanity check: either HTML or PNG has to be fully supported
+ if ((restrictions & Column::crHTMLMask) != Column::crHTMLFull &&
+ (restrictions & Column::crPNGMask) != Column::crPNGFull)
+ {
+ MessageBox(
+ 0,
+ i18n(muT("HistoryStats - Error")),
+ i18n(muT("An internal column configuration error occured. Please contact the author of this plugin.")),
+ MB_ICONERROR | MB_OK);
+ }
+
+ do
+ {
+ // HTML-only output but column doesn't support it
+ if (!bPNGOutput && !(restrictions & Column::crHTMLPartial))
+ {
+ if (pHelp)
+ {
+ pHelp->push_back(HelpPair());
+
+ pHelp->back().first = pCol->getTitle();
+ pHelp->back().first += muT(": ");
+ pHelp->back().first += i18n(muT("HTML output unsupported."));
+
+ pHelp->back().second = muT("");
+ }
+
+ ++nConflicts;
+ break;
+ }
+
+ // PNG output but only partial PNG support (enforce mode) -or-
+ // PNG output with alternative full HTML-only support (fallback mode)
+ if (bPNGOutput &&
+ (restrictions & Column::crPNGMask) == Column::crPNGPartial &&
+ (restrictions & Column::crHTMLMask) == Column::crHTMLFull &&
+ nPNGMode != Settings::pmPreferHTML)
+ {
+ if (pHelp)
+ {
+ pHelp->push_back(HelpPair());
+
+ pHelp->back().first = pCol->getTitle();
+ pHelp->back().first += muT(": ");
+
+ if (nPNGMode == Settings::pmHTMLFallBack)
+ {
+ pHelp->back().first += i18n(muT("Fallback to HTML due to setting."));
+ }
+ else
+ {
+ pHelp->back().first += i18n(muT("Setting ignored due to PNG output."));
+ }
+
+ pHelp->back().second = curDetails;
+ }
+
+ ++nConflicts;
+ break;
+ }
+ } while (false);
+ }
+ }
+
+ hItem = m_Columns.getNextItem(hItem);
+ }
+
+ return (nConflicts > 0);
+}
diff --git a/plugins/HistoryStats/src/dlgoption_subexclude.cpp b/plugins/HistoryStats/src/dlgoption_subexclude.cpp
new file mode 100644
index 0000000000..8014a0d00f
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption_subexclude.cpp
@@ -0,0 +1,339 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+#include "iconlib.h"
+#include "main.h"
+
+/*
+ * DlgOption::SubExclude
+ */
+
+DlgOption::SubExclude::SubExclude()
+ : m_hItemAll(NULL), m_bChanged(false)
+{
+}
+
+DlgOption::SubExclude::~SubExclude()
+{
+ // unlock exlucde contacts
+ g_bExcludeLock = false;
+}
+
+BOOL DlgOption::SubExclude::handleMsg(UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_WINDOWPOSCHANGED:
+ {
+ RECT rClient, rWnd;
+
+ GetClientRect(getHWnd(), &rClient);
+
+ // clist
+ rWnd = utils::getWindowRect(getHWnd(), IDC_CONTACTS);
+ rWnd.right = rClient.right;
+ rWnd.bottom = rClient.bottom;
+ utils::moveWindow(getHWnd(), IDC_CONTACTS, rWnd);
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+ NMHDR* p = reinterpret_cast<NMHDR*>(lParam);
+
+ if (p->idFrom != IDC_CONTACTS)
+ {
+ break;
+ }
+
+ HWND hCList = GetDlgItem(getHWnd(), IDC_CONTACTS);
+
+ switch (p->code)
+ {
+ case CLN_NEWCONTACT:
+ case CLN_LISTREBUILT:
+ updateAllContacts(hCList);
+ updateAllGroups(hCList, reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_ROOT, 0)), m_hItemAll);
+ break;
+
+ case CLN_CONTACTMOVED:
+ updateAllGroups(hCList, reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_ROOT, 0)), m_hItemAll);
+ break;
+
+ case CLN_OPTIONSCHANGED:
+ customizeList(hCList);
+ break;
+
+ case NM_CLICK:
+ {
+ NMCLISTCONTROL* pNM = reinterpret_cast<NMCLISTCONTROL*>(p);
+
+ if (pNM->iColumn == -1)
+ {
+ break;
+ }
+
+ DWORD dwHitFlags = 0;
+ HANDLE hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_HITTEST, reinterpret_cast<WPARAM>(&dwHitFlags), MAKELPARAM(pNM->pt.x, pNM->pt.y)));
+
+ if (!hItem || !(dwHitFlags & CLCHT_ONITEMEXTRA))
+ {
+ break;
+ }
+
+ int iImage = SendMessage(hCList, CLM_GETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(pNM->iColumn, 0));
+
+ if (iImage != 0xFF)
+ {
+ iImage = (iImage == 0) ? 1 : 0;
+
+ int itemType = SendMessage(hCList, CLM_GETITEMTYPE, reinterpret_cast<WPARAM>(hItem), 0);
+
+ if (itemType == CLCIT_CONTACT)
+ {
+ setAll(hCList, hItem, iImage, false);
+ }
+ else if (itemType == CLCIT_INFO)
+ {
+ setAll(hCList, hItem, iImage, true);
+ }
+ else if (itemType == CLCIT_GROUP)
+ {
+ if (hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_CHILD, reinterpret_cast<LPARAM>(hItem))))
+ {
+ setAll(hCList, hItem, iImage, true);
+ }
+ }
+
+ // update groups
+ updateAllGroups(hCList, reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_ROOT, 0)), m_hItemAll);
+
+ // mark as dirty
+ m_bChanged = true;
+ getParent()->settingsChanged();
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case WM_DESTROY:
+ IconLib::unregisterCallback(staticRecreateIcons, reinterpret_cast<LPARAM>(this));
+ ImageList_Destroy(reinterpret_cast<HIMAGELIST>(SendDlgItemMessage(getHWnd(), IDC_CONTACTS, CLM_GETEXTRAIMAGELIST, 0, 0)));
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgOption::SubExclude::onWMInitDialog()
+{
+ TranslateDialogDefault(getHWnd());
+
+ // init clist
+ HWND hCList = GetDlgItem(getHWnd(), IDC_CONTACTS);
+ HIMAGELIST hIml = ImageList_Create(OS::smIconCX(), OS::smIconCY(), OS::imageListColor() | ILC_MASK, 2, 0);
+ SendMessage(hCList, CLM_SETEXTRAIMAGELIST, 0, reinterpret_cast<LPARAM>(hIml));
+
+ staticRecreateIcons(reinterpret_cast<LPARAM>(this));
+ IconLib::registerCallback(staticRecreateIcons, reinterpret_cast<LPARAM>(this));
+
+ customizeList(hCList);
+ SendMessage(hCList, CLM_SETEXTRACOLUMNS, 1, 0);
+
+ CLCINFOITEM cii = { 0 };
+
+ cii.cbSize = sizeof(cii);
+ cii.flags = CLCIIF_GROUPFONT;
+ cii.pszText = i18n(muT("** All contacts **"));
+ m_hItemAll = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_ADDINFOITEM, 0, reinterpret_cast<LPARAM>(&cii)));
+
+ // lock exlucde contacts
+ g_bExcludeLock = true;
+}
+
+void DlgOption::SubExclude::staticRecreateIcons(LPARAM lParam)
+{
+ SubExclude* pDlg = reinterpret_cast<SubExclude*>(lParam);
+ HIMAGELIST hIml = reinterpret_cast<HIMAGELIST>(SendDlgItemMessage(pDlg->getHWnd(), IDC_CONTACTS, CLM_GETEXTRAIMAGELIST, 0, 0));
+
+ static IconLib::IconIndex ExtraIcons[] = {
+ IconLib::iiExcludeNo,
+ IconLib::iiExcludeYes,
+ };
+
+ ImageList_RemoveAll(hIml);
+
+ array_each_(i, ExtraIcons)
+ {
+ ImageList_AddIcon(hIml, IconLib::getIcon(ExtraIcons[i]));
+ }
+}
+
+void DlgOption::SubExclude::loadSettings()
+{
+ HWND hCList = GetDlgItem(getHWnd(), IDC_CONTACTS);
+
+ updateAllContacts(hCList);
+ updateAllGroups(hCList, reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_ROOT, 0)), m_hItemAll);
+
+ // reset dirty flag
+ m_bChanged = false;
+}
+
+void DlgOption::SubExclude::saveSettings()
+{
+ if (m_bChanged)
+ {
+ // update db
+ HWND hCList = GetDlgItem(getHWnd(), IDC_CONTACTS);
+ MirandaSettings db;
+
+ db.setModule(con::ModHistoryStats);
+
+ MCONTACT hContact = db_find_first();
+ while (hContact) {
+ HANDLE hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_FINDCONTACT, hContact, 0));
+ if (hItem) {
+ db.setContact(hContact);
+
+ int iImage = SendMessage(hCList, CLM_GETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, 0));
+ bool bExcludeOld = db.settingExists(con::SettExclude);
+
+ if (bExcludeOld && iImage == 0)
+ {
+ db.delSetting(con::SettExclude);
+ }
+ else if (!bExcludeOld && iImage == 1)
+ {
+ db.writeBool(con::SettExclude, true);
+ }
+ }
+
+ hContact = db_find_next(hContact);
+ }
+
+ // reset dirty flag
+ m_bChanged = false;
+ }
+}
+
+void DlgOption::SubExclude::customizeList(HWND hCList)
+{
+ SendMessage(hCList, CLM_SETBKBITMAP, 0, NULL);
+ SendMessage(hCList, CLM_SETBKCOLOR, GetSysColor(COLOR_WINDOW), 0);
+ SendMessage(hCList, CLM_SETGREYOUTFLAGS, 0, 0);
+ SendMessage(hCList, CLM_SETLEFTMARGIN, 2, 0);
+ SendMessage(hCList, CLM_SETINDENT, 10, 0);
+
+ for(int i = 0; i <= FONTID_MAX; ++i)
+ {
+ SendMessage(hCList, CLM_SETTEXTCOLOR, i, GetSysColor(COLOR_WINDOWTEXT));
+ }
+
+ SetWindowLong(hCList, GWL_STYLE, GetWindowLong(hCList, GWL_STYLE) | CLS_SHOWHIDDEN);
+}
+
+void DlgOption::SubExclude::updateAllGroups(HWND hCList, HANDLE hFirstItem, HANDLE hParentItem)
+{
+ bool bIconOn = true;
+ bool bHasChilds = false;
+
+ int typeOfFirst = SendMessage(hCList, CLM_GETITEMTYPE, reinterpret_cast<WPARAM>(hFirstItem), 0);
+
+ // groups
+ HANDLE hItem = (typeOfFirst == CLCIT_GROUP) ? hFirstItem : reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTGROUP, reinterpret_cast<LPARAM>(hFirstItem)));
+
+ // MEMO: no short-circuit, otherwise subgroups won't be updated
+ while (hItem /* && (bIconOn || !bHasChilds) */)
+ {
+ HANDLE hChildItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_CHILD, reinterpret_cast<LPARAM>(hItem)));
+
+ if (hChildItem)
+ {
+ updateAllGroups(hCList, hChildItem, hItem);
+ }
+
+ int iImage = SendMessage(hCList, CLM_GETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, 0));
+
+ bIconOn = bIconOn && (iImage != 0);
+ bHasChilds = bHasChilds || (iImage != 0xFF);
+
+ hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTGROUP, reinterpret_cast<LPARAM>(hItem)));
+ }
+
+ // contacts
+ hItem = (typeOfFirst == CLCIT_CONTACT) ? hFirstItem : reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTCONTACT, reinterpret_cast<LPARAM>(hFirstItem)));
+
+ while (hItem && (bIconOn || !bHasChilds))
+ {
+ int iImage = SendMessage(hCList, CLM_GETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, 0));
+
+ bIconOn = bIconOn && (iImage != 0);
+ bHasChilds = bHasChilds || (iImage != 0xFF);
+
+ hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTCONTACT, reinterpret_cast<LPARAM>(hItem)));
+ }
+
+ //set icon
+ SendMessage(hCList, CLM_SETEXTRAIMAGE, reinterpret_cast<WPARAM>(hParentItem), MAKELPARAM(0, bHasChilds ? (bIconOn ? 1 : 0) : 0xFF));
+}
+
+void DlgOption::SubExclude::updateAllContacts(HWND hCList)
+{
+ MirandaSettings db;
+
+ db.setModule(con::ModHistoryStats);
+
+ MCONTACT hContact = db_find_first();
+ while (hContact) {
+ HANDLE hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_FINDCONTACT, hContact, 0));
+ if (hItem) {
+ db.setContact(hContact);
+
+ if (SendMessage(hCList, CLM_GETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, 0)) == 0xFF)
+ SendMessage(hCList, CLM_SETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, db.settingExists(con::SettExclude) ? 1 : 0));
+ }
+
+ hContact = db_find_next(hContact);
+ }
+}
+
+void DlgOption::SubExclude::setAll(HWND hCList, HANDLE hFirstItem, int iImage, bool bIterate)
+{
+ int typeOfFirst = SendMessage(hCList, CLM_GETITEMTYPE, reinterpret_cast<WPARAM>(hFirstItem), 0);
+
+ if (bIterate)
+ {
+ // check groups
+ HANDLE hItem = (typeOfFirst == CLCIT_GROUP) ? hFirstItem : reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTGROUP, reinterpret_cast<LPARAM>(hFirstItem)));
+
+ while (hItem)
+ {
+ HANDLE hChildItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_CHILD, reinterpret_cast<LPARAM>(hItem)));
+
+ if (hChildItem)
+ {
+ setAll(hCList, hChildItem, iImage, true);
+ }
+
+ hItem = reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTGROUP, reinterpret_cast<LPARAM>(hItem)));
+ }
+ }
+
+ // check contacts
+ HANDLE hItem = (typeOfFirst == CLCIT_CONTACT) ? hFirstItem : reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTCONTACT, reinterpret_cast<LPARAM>(hFirstItem)));
+
+ while (hItem)
+ {
+ int iOldIcon = SendMessage(hCList, CLM_GETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, 0));
+
+ if (iOldIcon != 0xFF && iOldIcon != iImage)
+ {
+ SendMessage(hCList, CLM_SETEXTRAIMAGE, reinterpret_cast<WPARAM>(hItem), MAKELPARAM(0, iImage));
+ }
+
+ hItem = bIterate ? reinterpret_cast<HANDLE>(SendMessage(hCList, CLM_GETNEXTITEM, CLGN_NEXTCONTACT, reinterpret_cast<LPARAM>(hItem))) : NULL;
+ }
+}
diff --git a/plugins/HistoryStats/src/dlgoption_subglobal.cpp b/plugins/HistoryStats/src/dlgoption_subglobal.cpp
new file mode 100644
index 0000000000..907d6a374e
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption_subglobal.cpp
@@ -0,0 +1,458 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+#include "main.h"
+
+/*
+ * DlgOption::SubGlobal
+ */
+
+INT_PTR CALLBACK DlgOption::SubGlobal::staticInfoProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ {
+ TranslateDialogDefault(hDlg);
+
+ SendMessage(hDlg, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_HISTORYSTATS))));
+
+ utils::centerDialog(hDlg);
+
+ // fill with info
+ const SupportInfo& info = *reinterpret_cast<const SupportInfo*>(lParam);
+
+ SetDlgItemText(hDlg, IDC_PLUGIN, info.szPlugin);
+ SetDlgItemText(hDlg, IDC_FEATURES, i18n(info.szTeaser));
+ SetDlgItemText(hDlg, IDC_DESCRIPTION, i18n(info.szDescription));
+
+ static const WORD LinkIDs[] = { IDC_LINK2, IDC_LINK1 };
+
+ ext::string linkTexts = i18n(info.szLinkTexts);
+ ext::string linkURLs = info.szLinkURLs;
+ int nCurLink = 0;
+
+ if (!linkTexts.empty())
+ {
+ while (!linkTexts.empty() && nCurLink < array_len(LinkIDs))
+ {
+ ext::string::size_type posTexts = linkTexts.rfind(muC('|'));
+ ext::string::size_type posURLs = linkURLs.rfind(muC('|'));
+
+ if (posTexts == ext::string::npos || posURLs == ext::string::npos)
+ {
+ posTexts = posURLs = -1;
+ }
+
+ ext::string linkLabel = linkURLs.substr(posURLs + 1);
+
+ linkLabel += muT(" [");
+ linkLabel += linkTexts.substr(posTexts + 1);
+ linkLabel += muT("]");
+
+ SetDlgItemText(hDlg, LinkIDs[nCurLink], linkLabel.c_str());
+
+ linkTexts.erase((posTexts != -1) ? posTexts : 0);
+ linkURLs.erase((posURLs != -1) ? posURLs : 0);
+
+ ++nCurLink;
+ }
+ }
+
+ int nHeightAdd = 0;
+
+ while (nCurLink < array_len(LinkIDs))
+ {
+ HWND hLink = GetDlgItem(hDlg, LinkIDs[nCurLink]);
+ RECT rLink;
+
+ GetWindowRect(hLink, &rLink);
+ nHeightAdd += rLink.bottom - rLink.top;
+
+ ShowWindow(hLink, SW_HIDE);
+ EnableWindow(hLink, FALSE);
+
+ ++nCurLink;
+ }
+
+ if (nHeightAdd > 0)
+ {
+ RECT rDetails;
+
+ GetWindowRect(GetDlgItem(hDlg, IDC_DESCRIPTION), &rDetails);
+ SetWindowPos(GetDlgItem(hDlg, IDC_DESCRIPTION), NULL, 0, 0, rDetails.right - rDetails.left, rDetails.bottom - rDetails.top + nHeightAdd, SWP_NOMOVE | SWP_NOZORDER);
+ }
+ }
+ return TRUE;
+
+ case WM_COMMAND:
+ switch (LOWORD(wParam))
+ {
+ case IDOK:
+ EndDialog(hDlg, -1);
+ return TRUE;
+
+ case IDCANCEL:
+ EndDialog(hDlg, -1);
+ return TRUE;
+
+ case IDC_LINK1:
+ case IDC_LINK2:
+ if (HIWORD(wParam) == STN_CLICKED)
+ {
+ HWND hLink = reinterpret_cast<HWND>(lParam);
+ int nLen = GetWindowTextLength(hLink);
+ mu_text* szTitle = new mu_text[nLen + 1];
+
+ if (GetWindowText(hLink, szTitle, nLen + 1))
+ {
+ mu_text* szEndOfURL = (mu_text*)ext::strfunc::str(szTitle, muT(" ["));
+ if (szEndOfURL) {
+ *szEndOfURL = muC('\0');
+ g_pSettings->openURL(szTitle);
+ }
+ }
+
+ delete szTitle;
+ }
+ return TRUE;
+ }
+ break;
+
+ case WM_CTLCOLORSTATIC:
+ {
+ HWND hStatic = reinterpret_cast<HWND>(lParam);
+ mu_text szClassName[64];
+
+ if (GetClassName(hStatic, szClassName, array_len(szClassName)) && ext::strfunc::cmp(szClassName, WC_EDIT) == 0)
+ {
+ HDC hDC = reinterpret_cast<HDC>(wParam);
+
+ SetBkColor(hDC, GetSysColor(COLOR_WINDOW));
+ SetTextColor(hDC, GetSysColor(COLOR_WINDOWTEXT));
+
+ return reinterpret_cast<BOOL>(GetSysColorBrush(COLOR_WINDOW));
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+DlgOption::SubGlobal::SubGlobal()
+ : m_hOnStartup(NULL),
+ m_hShowMainMenu(NULL),
+ m_hShowMainMenuSub(NULL),
+ m_hShowContactMenu(NULL),
+ m_hShowContactMenuPseudo(NULL),
+ m_hGraphicsMode(NULL),
+ m_hGraphicsModePNG(NULL),
+ m_hPNGMode(NULL),
+ m_hThreadLowPriority(NULL),
+ m_hPathToBrowser(NULL),
+ m_bShowInfo(false),
+ m_nInfoHeight(0)
+{
+}
+
+DlgOption::SubGlobal::~SubGlobal()
+{
+ g_pSettings->setShowSupportInfo(m_bShowInfo);
+}
+
+BOOL DlgOption::SubGlobal::handleMsg(UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_WINDOWPOSCHANGED:
+ rearrangeControls();
+ return TRUE;
+
+ case WM_COMMAND:
+ if (LOWORD(wParam) == IDC_INFOLABEL && HIWORD(wParam) == STN_CLICKED)
+ {
+ m_bShowInfo = !m_bShowInfo;
+ toggleInfo();
+ return TRUE;
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+ NMHDR* p = reinterpret_cast<NMHDR*>(lParam);
+
+ if (p->idFrom == IDC_INFO)
+ {
+ NMTREEVIEW* pNM = reinterpret_cast<NMTREEVIEW*>(lParam);
+
+ if (p->code == NM_DBLCLK)
+ {
+ DWORD dwPoint = GetMessagePos();
+ POINTS pts = MAKEPOINTS(dwPoint);
+ TVHITTESTINFO tvhti = { { pts.x, pts.y } };
+
+ ScreenToClient(pNM->hdr.hwndFrom, &tvhti.pt);
+
+ if (TreeView_HitTest(pNM->hdr.hwndFrom, &tvhti) && tvhti.flags & TVHT_ONITEM)
+ {
+ TVITEM tvi;
+
+ tvi.mask = TVIF_HANDLE | TVIF_PARAM;
+ tvi.hItem = tvhti.hItem;
+
+ if (TreeView_GetItem(pNM->hdr.hwndFrom, &tvi) && tvi.lParam)
+ {
+ onShowSupportInfo(*reinterpret_cast<SupportInfo*>(tvi.lParam));
+ return TRUE;
+ }
+ }
+ }
+ else if (p->code == TVN_ITEMEXPANDING)
+ {
+ if (pNM->action == TVE_COLLAPSE || pNM->action == TVE_COLLAPSERESET ||
+ (pNM->action == TVE_TOGGLE && pNM->itemNew.state & TVIS_EXPANDED))
+ {
+ SetWindowLong(getHWnd(), DWLP_MSGRESULT, TRUE);
+ return TRUE;
+ }
+ }
+ }
+ else if (p->idFrom == IDC_OPTIONS)
+ {
+ if (p->code == OptionsCtrl::OCN_MODIFIED)
+ {
+ getParent()->settingsChanged();
+ getParent()->updateProblemInfo();
+ return TRUE;
+ }
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgOption::SubGlobal::onWMInitDialog()
+{
+ TranslateDialogDefault(getHWnd());
+
+ // init options control
+ m_Options << GetDlgItem(getHWnd(), IDC_OPTIONS);
+
+ // settings
+ OptionsCtrl::Item hTemp;
+
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Integration")), OptionsCtrl::OCF_ROOTGROUP);
+ /**/ m_hOnStartup = m_Options.insertCheck(hTemp, i18n(muT("Create statistics on Miranda IM's startup")));
+ /**/ m_hShowMainMenu = m_Options.insertCheck(hTemp, i18n(muT("Add menu items to main menu")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hShowMainMenuSub = m_Options.insertCheck(m_hShowMainMenu, i18n(muT("Put menu items into submenu")));
+ /**/ m_hShowContactMenu = m_Options.insertCheck(hTemp, i18n(muT("Add menu items to contact menu")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hShowContactMenuPseudo = m_Options.insertCheck(m_hShowContactMenu, i18n(muT("Don't hide menu items for pseudo protocols")));
+ /**/ m_hProtocols = m_Options.insertGroup(m_hShowContactMenu, i18n(muT("Hide menu items for protocol...")));
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Graphics")), OptionsCtrl::OCF_ROOTGROUP | OptionsCtrl::OCF_NODISABLECHILDS);
+ /**/ m_hGraphicsMode = m_Options.insertRadio(hTemp, NULL, i18n(muT("Only use HTML to simulate graphics")));
+ /**/ m_hGraphicsModePNG = m_Options.insertRadio(hTemp, m_hGraphicsMode, i18n(muT("Generate PNG files to represent graphics")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hPNGMode = m_Options.insertRadio(m_hGraphicsModePNG, NULL, i18n(muT("Fall back to HTML output, if column options require HTML output")));
+ /**/ m_Options.insertRadio(m_hGraphicsModePNG, m_hPNGMode, i18n(muT("Enforce PNG output, possibly ignoring some column options")));
+ /**/ m_Options.insertRadio(m_hGraphicsModePNG, m_hPNGMode, i18n(muT("Prefer HTML output over PNG output, if available")));
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Miscellaneous")), OptionsCtrl::OCF_ROOTGROUP);
+ /**/ m_hThreadLowPriority = m_Options.insertCheck(hTemp, i18n(muT("Generate statistics in background thread with low priority")));
+ /**/ m_hPathToBrowser = m_Options.insertEdit(hTemp, i18n(muT("Path to browser (leave blank for system default)")));
+
+ // insert known protocols
+ m_hHideContactMenuProtos.clear();
+
+ PROTOACCOUNT **protoList;
+ int protoCount;
+
+ if (mu::proto::enumProtocols(&protoCount, &protoList) == 0)
+ {
+ upto_each_(i, protoCount)
+ {
+ m_hHideContactMenuProtos.push_back(m_Options.insertCheck(
+ m_hProtocols,
+ Protocol::getDisplayName(protoList[i]->szModuleName).c_str(),
+ 0,
+ reinterpret_cast<DWORD>(protoList[i]->szModuleName)));
+ }
+ }
+
+ m_Options.ensureVisible(NULL);
+
+ // init support info list
+ initSupportInfo();
+
+ m_Options.checkItem(m_hOnStartup, true);
+
+ m_bShowInfo = g_pSettings->getShowSupportInfo();
+ toggleInfo();
+}
+
+void DlgOption::SubGlobal::loadSettings()
+{
+ // read settings from local settings store
+ Settings& localS = getParent()->getLocalSettings();
+
+ m_Options.checkItem (m_hOnStartup , localS.m_OnStartup );
+ m_Options.checkItem (m_hShowMainMenu , localS.m_ShowMainMenu );
+ m_Options.checkItem (m_hShowMainMenuSub , localS.m_ShowMainMenuSub );
+ m_Options.checkItem (m_hShowContactMenu , localS.m_ShowContactMenu );
+ m_Options.checkItem (m_hShowContactMenuPseudo, localS.m_ShowContactMenuPseudo);
+ m_Options.setRadioChecked(m_hGraphicsMode , localS.m_GraphicsMode );
+ m_Options.setRadioChecked(m_hPNGMode , localS.m_PNGMode );
+ m_Options.checkItem (m_hThreadLowPriority , localS.m_ThreadLowPriority );
+ m_Options.setEditString (m_hPathToBrowser , localS.m_PathToBrowser.c_str());
+
+ // 'set check' on hidden contact menu items protocols
+ citer_each_(std::vector<OptionsCtrl::Check>, i, m_hHideContactMenuProtos)
+ {
+ ext::a::string protoName = reinterpret_cast<const mu_ansi*>(m_Options.getItemData(*i));
+
+ m_Options.checkItem(*i, localS.m_HideContactMenuProtos.find(protoName) != localS.m_HideContactMenuProtos.end());
+ }
+
+ // check for availability of 'libpng'
+ if (!Canvas::hasPNG())
+ {
+ if (m_Options.isItemChecked(m_hGraphicsModePNG))
+ {
+ m_Options.setRadioChecked(m_hGraphicsMode, Settings::gmHTML);
+ }
+
+ m_Options.enableItem(m_hGraphicsModePNG, false);
+ }
+}
+
+void DlgOption::SubGlobal::saveSettings()
+{
+ Settings& localS = getParent()->getLocalSettings();
+
+ localS.m_OnStartup = m_Options.isItemChecked (m_hOnStartup );
+ localS.m_ShowMainMenu = m_Options.isItemChecked (m_hShowMainMenu );
+ localS.m_ShowMainMenuSub = m_Options.isItemChecked (m_hShowMainMenuSub );
+ localS.m_ShowContactMenu = m_Options.isItemChecked (m_hShowContactMenu );
+ localS.m_ShowContactMenuPseudo = m_Options.isItemChecked (m_hShowContactMenuPseudo);
+ localS.m_GraphicsMode = m_Options.getRadioChecked(m_hGraphicsMode );
+ localS.m_PNGMode = m_Options.getRadioChecked(m_hPNGMode );
+ localS.m_ThreadLowPriority = m_Options.isItemChecked (m_hThreadLowPriority );
+ localS.m_PathToBrowser = m_Options.getEditString (m_hPathToBrowser );
+
+ localS.m_HideContactMenuProtos.clear();
+ vector_each_(i, m_hHideContactMenuProtos)
+ {
+ if (m_Options.isItemChecked(m_hHideContactMenuProtos[i]))
+ {
+ localS.m_HideContactMenuProtos.insert(reinterpret_cast<mu_ansi*>(m_Options.getItemData(m_hHideContactMenuProtos[i])));
+ }
+ }
+}
+
+void DlgOption::SubGlobal::initSupportInfo()
+{
+ static const SupportInfo Infos[] = {
+ {
+ muT("MetaContacts"),
+ I18N(muT("Create statistics for meta-contacts and their subcontacts")),
+ I18N(muT("The following information are only relevant if your already use MetaContacts. In case you do please use version 0.9.10.6 or above.\r\n\r\nHistoryStats perfectly integrates with MetaContacts and is able to collect the data from the meta-contact as well as from the subcontacts. It is able to intelligently merge all subcontacts histories and more. You can configure MetContacts integration in the \"Input\" options.")),
+ I18N(muT("MetaContacts Plugin")),
+ muT("http://addons.miranda-im.org/details.php?action=viewfile&id=1595")
+ },
+ {
+ muT("Updater"),
+ I18N(muT("Automatically get updates of HistoryStats")),
+ I18N(muT("Use this plugin if you'd like to be automatically notified when a new version of HistoryStats is published. This plugin can install the updated version, too. As always, be sure to use a recent version though this shouldn't be a big problem with this plugin.")),
+ I18N(muT("Updater|Updater (Unicode)")),
+ muT("http://addons.miranda-im.org/details.php?action=viewfile&id=2254|http://addons.miranda-im.org/details.php?action=viewfile&id=2596")
+ },
+ {
+ muT("IcoLib"),
+ I18N(muT("Easily exchange icons in HistoryStats' user interface")),
+ I18N(muT("Use this plugin if you'd like to customize most of the icons HistoryStats' user interface. Please be sure to use version 0.0.1.0 or above. Otherwise HistoryStats won't show up in IcoLib's options. If you're running Miranda IM 0.7.0 alpha #3 or above you don't need a separate plugin for this.")),
+ I18N(muT("Icons Library Manager")),
+ muT("http://addons.miranda-im.org/details.php?action=viewfile&id=2700")
+ },
+ };
+
+ HWND hInfo = GetDlgItem(getHWnd(), IDC_INFO);
+
+ SetWindowLong(hInfo, GWL_STYLE, GetWindowLong(hInfo, GWL_STYLE) | TVS_NOHSCROLL);
+
+ // fill with data
+ TVINSERTSTRUCT tvi;
+
+ SendMessage(hInfo, WM_SETREDRAW, FALSE, 0);
+
+ tvi.hParent = TVI_ROOT;
+ tvi.hInsertAfter = TVI_LAST;
+ tvi.item.mask = TVIF_TEXT | TVIF_PARAM | TVIF_STATE;
+ tvi.item.lParam = 0;
+ tvi.item.state = TVIS_EXPANDED | TVIS_BOLD;
+ tvi.item.stateMask = TVIS_EXPANDED | TVIS_BOLD;
+
+ tvi.item.pszText = const_cast<mu_text*>(i18n(muT("Supported plugins (double-click to learn more):")));
+ tvi.hParent = TreeView_InsertItem(hInfo, &tvi);
+
+ tvi.item.stateMask &= ~TVIS_BOLD;
+
+ array_each_(i, Infos)
+ {
+ tvi.item.pszText = const_cast<mu_text*>(Infos[i].szPlugin);
+ tvi.item.lParam = reinterpret_cast<LPARAM>(&Infos[i]);
+
+ TreeView_InsertItem(hInfo, &tvi);
+ }
+
+ SendMessage(hInfo, WM_SETREDRAW, TRUE, 0);
+}
+
+void DlgOption::SubGlobal::rearrangeControls()
+{
+ RECT rClient, rWnd;
+ int offsetY;
+
+ if (m_nInfoHeight == 0)
+ {
+ m_nInfoHeight = utils::getWindowRect(getHWnd(), IDC_INFO).bottom;
+ m_nInfoHeight -= utils::getWindowRect(getHWnd(), IDC_INFOLABEL).bottom;
+ }
+
+ GetClientRect(getHWnd(), &rClient);
+
+ // support info list
+ rWnd = utils::getWindowRect(getHWnd(), IDC_INFO);
+ offsetY = rClient.bottom + (m_bShowInfo ? 0 : m_nInfoHeight) - rWnd.bottom;
+ OffsetRect(&rWnd, 0, offsetY);
+ rWnd.right = rClient.right;
+ utils::moveWindow(getHWnd(), IDC_INFO, rWnd);
+
+ // support info list label
+ rWnd = utils::getWindowRect(getHWnd(), IDC_INFOLABEL);
+ OffsetRect(&rWnd, 0, offsetY);
+ rWnd.right = rClient.right;
+ utils::moveWindow(getHWnd(), IDC_INFOLABEL, rWnd);
+
+ // options tree
+ rWnd = utils::getWindowRect(getHWnd(), m_Options);
+ rWnd.right = rClient.right;
+ rWnd.bottom += offsetY;
+ utils::moveWindow(m_Options, rWnd);
+}
+
+void DlgOption::SubGlobal::toggleInfo()
+{
+ HWND hInfo = GetDlgItem(getHWnd(), IDC_INFO);
+ const mu_text* szInfoLabelText = m_bShowInfo ? I18N(muT("HistoryStats supports several plugins. Click to hide info...")) : I18N(muT("HistoryStats supports several plugins. Click to learn more..."));
+
+ SetDlgItemText(getHWnd(), IDC_INFOLABEL, i18n(szInfoLabelText));
+ ShowWindow(hInfo, m_bShowInfo ? SW_SHOW : SW_HIDE);
+ EnableWindow(hInfo, BOOL_(m_bShowInfo));
+
+ rearrangeControls();
+}
+
+void DlgOption::SubGlobal::onShowSupportInfo(const SupportInfo& info)
+{
+ DialogBoxParam(g_hInst, MAKEINTRESOURCE(IDD_SUPPORTINFO), getHWnd(), staticInfoProc, reinterpret_cast<LPARAM>(&info));
+}
diff --git a/plugins/HistoryStats/src/dlgoption_subinput.cpp b/plugins/HistoryStats/src/dlgoption_subinput.cpp
new file mode 100644
index 0000000000..e1d51bb2c3
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption_subinput.cpp
@@ -0,0 +1,202 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+#include "protocol.h"
+
+/*
+ * DlgOption::SubInput
+ */
+
+DlgOption::SubInput::SubInput()
+ : m_hChatSessionMinDur(NULL)
+ , m_hChatSessionTimeout(NULL)
+ , m_hAverageMinTime(NULL)
+ , m_hWordDelimiters(NULL)
+ , m_hMetaContactsMode(NULL)
+ , m_hMergeContacts(NULL)
+ , m_hMergeContactsGroups(NULL)
+ , m_hMergeMode(NULL)
+ , m_hProtocols(NULL)
+ , m_hIgnoreOlder(NULL)
+ , m_hIgnoreBefore(NULL)
+ , m_hIgnoreAfter(NULL)
+ , m_hFilterRawRTF(NULL)
+ , m_hFilterBBCodes(NULL)
+{
+}
+
+DlgOption::SubInput::~SubInput()
+{
+}
+
+BOOL DlgOption::SubInput::handleMsg(UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_WINDOWPOSCHANGED:
+ {
+ RECT rClient, rWnd;
+
+ GetClientRect(getHWnd(), &rClient);
+
+ // options tree
+ rWnd = utils::getWindowRect(getHWnd(), m_Options);
+ rWnd.right = rClient.right;
+ rWnd.bottom = rClient.bottom;
+ utils::moveWindow(m_Options, rWnd);
+ }
+ return TRUE;
+
+ case WM_NOTIFY:
+ {
+ NMHDR* p = reinterpret_cast<NMHDR*>(lParam);
+
+ if (p->idFrom == IDC_OPTIONS)
+ {
+ if (p->code == OptionsCtrl::OCN_MODIFIED)
+ {
+ getParent()->settingsChanged();
+ return TRUE;
+ }
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgOption::SubInput::onWMInitDialog()
+{
+ TranslateDialogDefault(getHWnd());
+
+ // init options control
+ m_Options << GetDlgItem(getHWnd(), IDC_OPTIONS);
+
+ // settings
+ OptionsCtrl::Item hTemp, hTemp2;
+
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("History interpretation")), OptionsCtrl::OCF_ROOTGROUP);
+ /**/ m_hChatSessionMinDur = m_Options.insertEdit(hTemp, i18n(muT("Time a chat session must last to be counted (seconds)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hChatSessionTimeout = m_Options.insertEdit(hTemp, i18n(muT("Time between two chat sessions (seconds)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hAverageMinTime = m_Options.insertEdit(hTemp, i18n(muT("Minimum time to assume when calculating average (days)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hWordDelimiters = m_Options.insertEdit(hTemp, i18n(muT("Word delimiting characters")));
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Contact filtering")), OptionsCtrl::OCF_ROOTGROUP | OptionsCtrl::OCF_NODISABLECHILDS);
+ /**/ m_hProtocols = m_Options.insertGroup(hTemp, i18n(muT("Ignore all contacts with protocol...")));
+
+ if (mu::metacontacts::_available())
+ {
+ /**/hTemp2 = m_Options.insertGroup(hTemp, i18n(muT("History read mode for MetaContacts")), mu::metacontacts::_available() ? 0 : OptionsCtrl::OCF_DISABLED);
+ /**/ m_hMetaContactsMode = m_Options.insertRadio(hTemp2, NULL, i18n(muT("Use only meta-contact's history")));
+ /**/ m_Options.insertRadio(hTemp2, m_hMetaContactsMode, i18n(muT("Use only subcontacts' histories (for one meta-contact)")));
+ /**/ m_Options.insertRadio(hTemp2, m_hMetaContactsMode, i18n(muT("Use meta-contact's history and its subcontacts' histories")));
+ /**/ m_Options.insertRadio(hTemp2, m_hMetaContactsMode, i18n(muT("Treat meta-contacts and subcontacts as normal contacts")));
+ }
+
+ /**/ m_hMergeContacts = m_Options.insertCheck(hTemp, i18n(muT("Merge contacts with same name")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hMergeContactsGroups = m_Options.insertCheck(m_hMergeContacts, i18n(muT("Only merge if contacts are in the same group")));
+ /**/ hTemp2 = m_Options.insertGroup(hTemp, i18n(muT("Duplicate detection when reading merged contacts")));
+ /**/ m_hMergeMode = m_Options.insertRadio(hTemp2, NULL, i18n(muT("Merge events (tolerant)")));
+ /**/ m_Options.insertRadio(hTemp2, m_hMergeMode, i18n(muT("Merge events (strict)")));
+ /**/ m_Options.insertRadio(hTemp2, m_hMergeMode, i18n(muT("Don't merge events")));
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Message filtering")), OptionsCtrl::OCF_ROOTGROUP);
+ /**/ hTemp2 = m_Options.insertGroup(hTemp, i18n(muT("Ignore messages...")));
+ /**/ m_hIgnoreOlder = m_Options.insertEdit(hTemp2, i18n(muT("...older than (days, 0=no limit)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hIgnoreBefore = m_Options.insertDateTime(hTemp2, i18n(muT("...before date (none=no limit)")), 0, muT("%Y-%m-%d"), OptionsCtrl::OCF_ALLOWNONE);
+ /**/ m_hIgnoreAfter = m_Options.insertDateTime(hTemp2, i18n(muT("...after date (none=no limit)")), 0, muT("%Y-%m-%d"), OptionsCtrl::OCF_ALLOWNONE);
+ /**/ m_hFilterRawRTF = m_Options.insertCheck(hTemp, i18n(muT("Strip raw RTF control sequences from message")));
+ /**/ m_hFilterBBCodes = m_Options.insertCheck(hTemp, i18n(muT("Strip BBCode tags from messages")));
+
+ // insert known protocols
+ m_hProtosIgnore.clear();
+
+ PROTOACCOUNT **protoList;
+ int protoCount;
+
+ if (mu::proto::enumProtocols(&protoCount, &protoList) == 0)
+ {
+ upto_each_(i, protoCount)
+ {
+ m_hProtosIgnore.push_back(m_Options.insertCheck(
+ m_hProtocols,
+ Protocol::getDisplayName(protoList[i]->szModuleName).c_str(),
+ 0,
+ reinterpret_cast<DWORD>(protoList[i]->szModuleName)));
+ }
+ }
+
+ // diable filtering raw RTF if 'rtfconv.dll' isn't available
+ if (!RTFFilter::available())
+ {
+ m_Options.enableItem(m_hFilterRawRTF, false);
+ }
+
+ m_Options.ensureVisible(NULL);
+}
+
+void DlgOption::SubInput::loadSettings()
+{
+ // read settings from local settings store
+ Settings& localS = getParent()->getLocalSettings();
+
+ m_Options.setEditNumber (m_hChatSessionMinDur , localS.m_ChatSessionMinDur );
+ m_Options.setEditNumber (m_hChatSessionTimeout , localS.m_ChatSessionTimeout );
+ m_Options.setEditNumber (m_hAverageMinTime , localS.m_AverageMinTime );
+ m_Options.setEditString (m_hWordDelimiters , localS.m_WordDelimiters.c_str());
+ m_Options.checkItem (m_hMergeContacts , localS.m_MergeContacts );
+ m_Options.checkItem (m_hMergeContactsGroups, localS.m_MergeContactsGroups );
+ m_Options.setRadioChecked(m_hMergeMode , localS.m_MergeMode );
+ m_Options.setEditNumber (m_hIgnoreOlder , localS.m_IgnoreOld );
+ m_Options.setDateTimeStr (m_hIgnoreBefore , localS.m_IgnoreBefore );
+ m_Options.setDateTimeStr (m_hIgnoreAfter , localS.m_IgnoreAfter );
+ m_Options.checkItem (m_hFilterRawRTF , localS.m_FilterRawRTF );
+ m_Options.checkItem (m_hFilterBBCodes , localS.m_FilterBBCodes );
+
+ if (m_hMetaContactsMode)
+ {
+ m_Options.setRadioChecked(m_hMetaContactsMode, localS.m_MetaContactsMode);
+ }
+
+ // 'set check' on ignored protocols
+ citer_each_(std::vector<OptionsCtrl::Check>, i, m_hProtosIgnore)
+ {
+ ext::a::string protoName = reinterpret_cast<const mu_ansi*>(m_Options.getItemData(*i));
+
+ m_Options.checkItem(*i, localS.m_ProtosIgnore.find(protoName) != localS.m_ProtosIgnore.end());
+ }
+
+ // update UI: MetaContacts-specific, see onInitDialog()
+ // <nothing to do>
+}
+
+void DlgOption::SubInput::saveSettings()
+{
+ Settings& localS = getParent()->getLocalSettings();
+
+ localS.m_ChatSessionMinDur = m_Options.getEditNumber (m_hChatSessionMinDur );
+ localS.m_ChatSessionTimeout = m_Options.getEditNumber (m_hChatSessionTimeout );
+ localS.m_AverageMinTime = m_Options.getEditNumber (m_hAverageMinTime );
+ localS.m_WordDelimiters = m_Options.getEditString (m_hWordDelimiters );
+ localS.m_MergeContacts = m_Options.isItemChecked (m_hMergeContacts );
+ localS.m_MergeContactsGroups = m_Options.isItemChecked (m_hMergeContactsGroups);
+ localS.m_MergeMode = m_Options.getRadioChecked(m_hMergeMode );
+ localS.m_IgnoreOld = m_Options.getEditNumber (m_hIgnoreOlder );
+ localS.m_IgnoreBefore = m_Options.getDateTimeStr (m_hIgnoreBefore );
+ localS.m_IgnoreAfter = m_Options.getDateTimeStr (m_hIgnoreAfter );
+ localS.m_FilterRawRTF = m_Options.isItemChecked (m_hFilterRawRTF );
+ localS.m_FilterBBCodes = m_Options.isItemChecked (m_hFilterBBCodes );
+
+ if (m_hMetaContactsMode)
+ {
+ localS.m_MetaContactsMode = m_Options.getRadioChecked(m_hMetaContactsMode);
+ }
+
+ localS.m_ProtosIgnore.clear();
+ vector_each_(i, m_hProtosIgnore)
+ {
+ if (m_Options.isItemChecked(m_hProtosIgnore[i]))
+ {
+ localS.m_ProtosIgnore.insert(reinterpret_cast<mu_ansi*>(m_Options.getItemData(m_hProtosIgnore[i])));
+ }
+ }
+}
diff --git a/plugins/HistoryStats/src/dlgoption_suboutput.cpp b/plugins/HistoryStats/src/dlgoption_suboutput.cpp
new file mode 100644
index 0000000000..aa10236451
--- /dev/null
+++ b/plugins/HistoryStats/src/dlgoption_suboutput.cpp
@@ -0,0 +1,350 @@
+#include "_globals.h"
+#include "dlgoption.h"
+
+/*
+ * DlgOption::SubOutput
+ */
+
+DlgOption::SubOutput::SubOutput()
+ : m_hRemoveEmptyContacts(NULL)
+ , m_hRemoveInChatsZero(NULL)
+ , m_hRemoveInBytesZero(NULL)
+ , m_hRemoveOutChatsZero(NULL)
+ , m_hRemoveOutBytesZero(NULL)
+ , m_hOmitContacts(NULL)
+ , m_hOmitByValue(NULL)
+ , m_hOmitByValueData(NULL)
+ , m_hOmitByValueLimit(NULL)
+ , m_hOmitByTime(NULL)
+ , m_hOmitByTimeDays(NULL)
+ , m_hOmitByRank(NULL)
+ , m_hOmitNumOnTop(NULL)
+ , m_hOmittedInTotals(NULL)
+ , m_hOmittedInExtraRow(NULL)
+ , m_hCalcTotals(NULL)
+ , m_hTableHeader(NULL)
+ , m_hTableHeaderRepeat(NULL)
+ , m_hTableHeaderVerbose(NULL)
+ , m_hHeaderTooltips(NULL)
+ , m_hHeaderTooltipsIfCustom(NULL)
+ , m_hSort(NULL)
+ , m_hNick(NULL)
+ , m_hOutputVariables(NULL)
+ , m_hOutputFile(NULL)
+ , m_hOutputExtraToFolder(NULL)
+ , m_hOutputExtraFolder(NULL)
+ , m_hOverwriteAlways(NULL)
+ , m_hAutoOpenOptions(NULL)
+ , m_hAutoOpenStartup(NULL)
+ , m_hAutoOpenMenu(NULL)
+{
+ array_each_(i, m_hSortBy)
+ {
+ m_hSortBy[i] = NULL;
+ }
+
+ array_each_(i, m_hSortDir)
+ {
+ m_hSortDir[i] = NULL;
+ }
+}
+
+DlgOption::SubOutput::~SubOutput()
+{
+}
+
+BOOL DlgOption::SubOutput::handleMsg(UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (msg)
+ {
+ case WM_WINDOWPOSCHANGED:
+ {
+ RECT rClient, rWnd;
+
+ GetClientRect(getHWnd(), &rClient);
+
+ // options tree
+ rWnd = utils::getWindowRect(getHWnd(), m_Options);
+ rWnd.right = rClient.right;
+ rWnd.bottom = rClient.bottom;
+ utils::moveWindow(m_Options, rWnd);
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+ NMHDR* p = reinterpret_cast<NMHDR*>(lParam);
+
+ if (p->idFrom == IDC_OPTIONS)
+ {
+ if (p->code == OptionsCtrl::OCN_MODIFIED)
+ {
+ OptionsCtrl::NMOPTIONSCTRL* pNM = reinterpret_cast<OptionsCtrl::NMOPTIONSCTRL*>(lParam);
+
+ onChanged(pNM->hItem);
+ return TRUE;
+ }
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+void DlgOption::SubOutput::onWMInitDialog()
+{
+ TranslateDialogDefault(getHWnd());
+
+ // init option tree(s)
+ m_Options << GetDlgItem(getHWnd(), IDC_OPTIONS);
+
+ // settings
+ OptionsCtrl::Item hTemp;
+ OptionsCtrl::Item hTemp2;
+
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Contact filtering and totals")), OptionsCtrl::OCF_ROOTGROUP | OptionsCtrl::OCF_NODISABLECHILDS);
+ /**/ m_hRemoveEmptyContacts = m_Options.insertCheck(hTemp, i18n(muT("Remove contacts with empty history")));
+ /**/ m_hRemoveOutChatsZero = m_Options.insertCheck(hTemp, i18n(muT("Remove contacts with only incoming chats")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hRemoveOutBytesZero = m_Options.insertCheck(m_hRemoveOutChatsZero, i18n(muT("Remove only if you never answered")));
+ /**/ m_hRemoveInChatsZero = m_Options.insertCheck(hTemp, i18n(muT("Remove contacts with only outgoing chats")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hRemoveInBytesZero = m_Options.insertCheck(m_hRemoveInChatsZero, i18n(muT("Remove only if contact never answered")));
+ /**/ m_hOmitContacts = m_Options.insertCheck(hTemp, i18n(muT("Limit number of contacts in statistics")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ hTemp2 = m_Options.insertGroup(m_hOmitContacts, i18n(muT("Criteria")));
+ /**/ m_hOmitByValue = m_Options.insertCheck(hTemp2, i18n(muT("Omit contacts that didn't produce a certain amount of data")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hOmitByValueData = m_Options.insertCombo(m_hOmitByValue, i18n(muT("Omit if")));
+ /**/ m_hOmitByValueLimit = m_Options.insertEdit(m_hOmitByValue, i18n(muT("...is less than")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hOmitByTime = m_Options.insertCheck(hTemp2, i18n(muT("Omit contacts that were inactive for some time")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hOmitByTimeDays = m_Options.insertEdit(m_hOmitByTime, i18n(muT("Maximum inactivity time (days)")), muT(""), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hOmitByRank = m_Options.insertCheck(hTemp2, i18n(muT("Omit all contacts not in \"Top n\"")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hOmitNumOnTop = m_Options.insertEdit(m_hOmitByRank, i18n(muT("Number of contacts in \"Top n\"")), muT("10"), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hOmittedInTotals = m_Options.insertCheck(m_hOmitContacts, i18n(muT("Include omitted contacts in totals")));
+ /**/ m_hOmittedInExtraRow = m_Options.insertCheck(m_hOmitContacts, i18n(muT("Include totals of omitted contacts in additional row")));
+ /**/ m_hCalcTotals = m_Options.insertCheck(hTemp, i18n(muT("Include totals in statistics")));
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("Table header")), OptionsCtrl::OCF_ROOTGROUP | OptionsCtrl::OCF_NODISABLECHILDS);
+ /**/ m_hTableHeader = m_Options.insertCheck(hTemp, i18n(muT("Output header")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK | OptionsCtrl::OCF_DISABLED | OptionsCtrl::OCF_NODISABLECHILDS);
+ /**/ m_hTableHeaderRepeat = m_Options.insertEdit(m_hTableHeader, i18n(muT("Repeat header every n contacts (0=don't repeat)")), muT("0"), OptionsCtrl::OCF_NUMBER);
+ /**/ m_hTableHeaderVerbose = m_Options.insertCheck(m_hTableHeader, i18n(muT("Make column titles more verbose")));
+ /**/ m_hHeaderTooltips = m_Options.insertCheck(m_hTableHeader, i18n(muT("Show tooltips with detailed information in column titles")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hHeaderTooltipsIfCustom = m_Options.insertCheck(m_hHeaderTooltips, i18n(muT("Only show if a custom title was entered or if titles are not verbose")));
+ /**/m_hSort = m_Options.insertGroup(NULL, i18n(muT("Sorting")), OptionsCtrl::OCF_ROOTGROUP);
+ /**/hTemp = m_Options.insertGroup(NULL, i18n(muT("HTML file generation")), OptionsCtrl::OCF_ROOTGROUP);
+ /**/ m_hNick = m_Options.insertEdit(hTemp, i18n(muT("Own nick for statistics")));
+ /**/ hTemp2 = m_Options.insertGroup(hTemp, i18n(muT("Output files and folders")));
+ /**/ m_hOutputVariables = m_Options.insertCheck(hTemp2, i18n(muT("Substitute variables in output file name and subfolder for additional files")));
+ /**/ m_hOutputFile = m_Options.insertEdit(hTemp2, i18n(muT("Output file")));
+ /**/ m_hOutputExtraToFolder = m_Options.insertCheck(hTemp2, i18n(muT("Output additional files to subfolder")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK);
+ /**/ m_hOutputExtraFolder = m_Options.insertEdit(m_hOutputExtraToFolder, i18n(muT("Subfolder for additional files")));
+ /**/ m_hOverwriteAlways = m_Options.insertCheck(hTemp2, i18n(muT("Always overwrite already existing files (dangerous!)")));
+ /**/ hTemp2 = m_Options.insertGroup(hTemp, i18n(muT("Auto open statistics after being generated...")));
+ /**/ m_hAutoOpenOptions = m_Options.insertCheck(hTemp2, i18n(muT("...via button \"Create statistics\" in options")));
+ /**/ m_hAutoOpenStartup = m_Options.insertCheck(hTemp2, i18n(muT("...on Miranda IM's startup")));
+ /**/ m_hAutoOpenMenu = m_Options.insertCheck(hTemp2, i18n(muT("...via Miranda IM's main menu")));
+
+ // our sort levels
+ static const mu_text* sortLevels[Settings::cNumSortLevels] = {
+ I18N(muT("First sort by")),
+ I18N(muT("Then sort by")),
+ I18N(muT("Finally sort by"))
+ };
+
+ // our sort keys
+ static const struct {
+ int key;
+ mu_text* desc;
+ } sortKeys[] = {
+ { Settings::skNick , I18N(muT("Nick")) },
+ { Settings::skProtocol , I18N(muT("Protocol")) },
+ { Settings::skGroup , I18N(muT("Group")) },
+ { Settings::skBytesIn , I18N(muT("Characters (incoming, absolute)")) },
+ { Settings::skBytesOut , I18N(muT("Characters (outgoing, absolute)")) },
+ { Settings::skBytesTotal , I18N(muT("Characters (all, absolute)")) },
+ { Settings::skBytesInAvg , I18N(muT("Characters (incoming, average)")) },
+ { Settings::skBytesOutAvg , I18N(muT("Characters (outgoing, average)")) },
+ { Settings::skBytesTotalAvg , I18N(muT("Characters (all, average)")) },
+ { Settings::skMessagesIn , I18N(muT("Messages (incoming, absolute)")) },
+ { Settings::skMessagesOut , I18N(muT("Messages (outgoing, absolute)")) },
+ { Settings::skMessagesTotal , I18N(muT("Messages (all, absolute)")) },
+ { Settings::skMessagesInAvg , I18N(muT("Messages (incoming, average)")) },
+ { Settings::skMessagesOutAvg , I18N(muT("Messages (outgoing, average)")) },
+ { Settings::skMessagesTotalAvg , I18N(muT("Messages (all, average)")) },
+ { Settings::skChatsIn , I18N(muT("Chats (incoming, absolute)")) },
+ { Settings::skChatsOut , I18N(muT("Chats (outgoing, absolute)")) },
+ { Settings::skChatsTotal , I18N(muT("Chats (all, absolute)")) },
+ { Settings::skChatsInAvg , I18N(muT("Chats (incoming, average)")) },
+ { Settings::skChatsOutAvg , I18N(muT("Chats (outgoing, average)")) },
+ { Settings::skChatsTotalAvg , I18N(muT("Chats (all, average)")) },
+ { Settings::skChatDurationTotal , I18N(muT("Chat duration (total)")) },
+ { Settings::skChatDurationMin , I18N(muT("Chat duration (minimum)")) },
+ { Settings::skChatDurationAvg , I18N(muT("Chat duration (average)")) },
+ { Settings::skChatDurationMax , I18N(muT("Chat duration (maximum)")) },
+ { Settings::skTimeOfFirstMessage, I18N(muT("Time of first message to/from contact")) },
+ { Settings::skTimeOfLastMessage , I18N(muT("Time of last message to/from contact")) },
+ };
+
+ m_SortKeyToIndex.resize(Settings::skLAST - Settings::skFIRST + 1);
+ m_IndexToSortKey.resize(array_len(sortKeys));
+
+ array_each_(i, sortKeys)
+ {
+ m_IndexToSortKey[i] = sortKeys[i].key;
+ m_SortKeyToIndex[sortKeys[i].key] = i;
+ }
+
+ array_each_(i, sortLevels)
+ {
+ m_hSortBy[i] = m_Options.insertCombo(m_hSort, i18n(sortLevels[i]), (i == 0) ? 0 : OptionsCtrl::OCF_DISABLECHILDSONINDEX0);
+
+ if (i != 0)
+ {
+ m_Options.addComboItem(m_hSortBy[i], i18n(muT("(nothing)")));
+ }
+
+ array_each_(j, sortKeys)
+ {
+ m_Options.addComboItem(m_hSortBy[i], i18n(sortKeys[j].desc));
+ }
+
+ /**/m_hSortDir[i] = m_Options.insertRadio(m_hSortBy[i], NULL, i18n(muT("Ascending")));
+ /**/ m_Options.insertRadio(m_hSortBy[i], m_hSortDir[i], i18n(muT("Descending")));
+ }
+
+ // our "omit by value" data
+ static const mu_text* omitData[] = {
+ I18N(muT("Characters (incoming, absolute)")),
+ I18N(muT("Characters (outgoing, absolute)")),
+ I18N(muT("Characters (all, absolute)")),
+ I18N(muT("Characters (incoming, average per week)")),
+ I18N(muT("Characters (outgoing, average per week)")),
+ I18N(muT("Characters (all, average per week)")),
+ I18N(muT("Messages (incoming, absolute)")),
+ I18N(muT("Messages (outgoing, absolute)")),
+ I18N(muT("Messages (all, absolute)")),
+ I18N(muT("Messages (incoming, average per week)")),
+ I18N(muT("Messages (outgoing, average per week)")),
+ I18N(muT("Messages (all, average per week)")),
+ I18N(muT("Chats (incoming, absolute)")),
+ I18N(muT("Chats (outgoing, absolute)")),
+ I18N(muT("Chats (all, absolute)")),
+ I18N(muT("Chats (incoming, average per week)")),
+ I18N(muT("Chats (outgoing, average per week)")),
+ I18N(muT("Chats (all, average per week)")),
+ I18N(muT("Chat duration (total, hours)")),
+ };
+
+ array_each_(i, omitData)
+ {
+ m_Options.addComboItem(m_hOmitByValueData, i18n(omitData[i]));
+ }
+
+ m_Options.ensureVisible(NULL);
+}
+
+void DlgOption::SubOutput::loadSettings()
+{
+ // read settings from local settings store
+ Settings& localS = getParent()->getLocalSettings();
+
+ m_Options.checkItem (m_hRemoveEmptyContacts , localS.m_RemoveEmptyContacts );
+ m_Options.checkItem (m_hRemoveOutChatsZero , localS.m_RemoveOutChatsZero );
+ m_Options.checkItem (m_hRemoveOutBytesZero , localS.m_RemoveOutBytesZero );
+ m_Options.checkItem (m_hRemoveInChatsZero , localS.m_RemoveInChatsZero );
+ m_Options.checkItem (m_hRemoveInBytesZero , localS.m_RemoveInBytesZero );
+ m_Options.checkItem (m_hOmitContacts , localS.m_OmitContacts );
+ m_Options.checkItem (m_hOmitByValue , localS.m_OmitByValue );
+ m_Options.setComboSelected(m_hOmitByValueData , localS.m_OmitByValueData );
+ m_Options.setEditNumber (m_hOmitByValueLimit , localS.m_OmitByValueLimit );
+ m_Options.checkItem (m_hOmitByTime , localS.m_OmitByTime );
+ m_Options.setEditNumber (m_hOmitByTimeDays , localS.m_OmitByTimeDays );
+ m_Options.checkItem (m_hOmitByRank , localS.m_OmitByRank );
+ m_Options.setEditNumber (m_hOmitNumOnTop , localS.m_OmitNumOnTop );
+ m_Options.checkItem (m_hOmittedInTotals , localS.m_OmittedInTotals );
+ m_Options.checkItem (m_hOmittedInExtraRow , localS.m_OmittedInExtraRow );
+ m_Options.checkItem (m_hCalcTotals , localS.m_CalcTotals );
+ m_Options.checkItem (m_hTableHeader , localS.m_TableHeader );
+ m_Options.setEditNumber (m_hTableHeaderRepeat , localS.m_TableHeaderRepeat );
+ m_Options.checkItem (m_hTableHeaderVerbose , localS.m_TableHeaderVerbose );
+ m_Options.checkItem (m_hHeaderTooltips , localS.m_HeaderTooltips );
+ m_Options.checkItem (m_hHeaderTooltipsIfCustom, localS.m_HeaderTooltipsIfCustom );
+ m_Options.setEditString (m_hNick , localS.m_OwnNick.c_str() );
+ m_Options.checkItem (m_hOutputVariables , localS.m_OutputVariables );
+ m_Options.setEditString (m_hOutputFile , localS.m_OutputFile.c_str() );
+ m_Options.checkItem (m_hOutputExtraToFolder , localS.m_OutputExtraToFolder );
+ m_Options.setEditString (m_hOutputExtraFolder , localS.m_OutputExtraFolder.c_str());
+ m_Options.checkItem (m_hOverwriteAlways , localS.m_OverwriteAlways );
+ m_Options.checkItem (m_hAutoOpenOptions , localS.m_AutoOpenOptions );
+ m_Options.checkItem (m_hAutoOpenStartup , localS.m_AutoOpenStartup );
+ m_Options.checkItem (m_hAutoOpenMenu , localS.m_AutoOpenMenu );
+
+ // our sort keys
+ upto_each_(i, Settings::cNumSortLevels)
+ {
+ int by = (localS.m_Sort[i].by >= 0 && localS.m_Sort[i].by < m_SortKeyToIndex.size()) ? m_SortKeyToIndex[localS.m_Sort[i].by] : -1;
+
+ m_Options.setComboSelected(m_hSortBy[i], by + ((i != 0) ? 1 : 0));
+ m_Options.setRadioChecked(m_hSortDir[i], localS.m_Sort[i].asc ? 0 : 1);
+ }
+
+ // update UI
+ onChanged(m_hSortBy[1]);
+ onChanged(m_hCalcTotals);
+}
+
+void DlgOption::SubOutput::saveSettings()
+{
+ Settings& localS = getParent()->getLocalSettings();
+
+ localS.m_RemoveEmptyContacts = m_Options.isItemChecked (m_hRemoveEmptyContacts );
+ localS.m_RemoveOutChatsZero = m_Options.isItemChecked (m_hRemoveOutChatsZero );
+ localS.m_RemoveOutBytesZero = m_Options.isItemChecked (m_hRemoveOutBytesZero );
+ localS.m_RemoveInChatsZero = m_Options.isItemChecked (m_hRemoveInChatsZero );
+ localS.m_RemoveInBytesZero = m_Options.isItemChecked (m_hRemoveInBytesZero );
+ localS.m_OmitContacts = m_Options.isItemChecked (m_hOmitContacts );
+ localS.m_OmitByValue = m_Options.isItemChecked (m_hOmitByValue );
+ localS.m_OmitByValueData = m_Options.getComboSelected(m_hOmitByValueData );
+ localS.m_OmitByValueLimit = m_Options.getEditNumber (m_hOmitByValueLimit );
+ localS.m_OmitByTime = m_Options.isItemChecked (m_hOmitByTime );
+ localS.m_OmitByTimeDays = m_Options.getEditNumber (m_hOmitByTimeDays );
+ localS.m_OmitByRank = m_Options.isItemChecked (m_hOmitByRank );
+ localS.m_OmitNumOnTop = m_Options.getEditNumber (m_hOmitNumOnTop );
+ localS.m_OmittedInTotals = m_Options.isItemChecked (m_hOmittedInTotals );
+ localS.m_OmittedInExtraRow = m_Options.isItemChecked (m_hOmittedInExtraRow );
+ localS.m_CalcTotals = m_Options.isItemChecked (m_hCalcTotals );
+ localS.m_TableHeader = m_Options.isItemChecked (m_hTableHeader );
+ localS.m_TableHeaderRepeat = m_Options.getEditNumber (m_hTableHeaderRepeat );
+ localS.m_TableHeaderVerbose = m_Options.isItemChecked (m_hTableHeaderVerbose );
+ localS.m_HeaderTooltips = m_Options.isItemChecked (m_hHeaderTooltips );
+ localS.m_HeaderTooltipsIfCustom = m_Options.isItemChecked (m_hHeaderTooltipsIfCustom);
+
+ upto_each_(i, Settings::cNumSortLevels)
+ {
+ int index = m_Options.getComboSelected(m_hSortBy[i]) - ((i != 0) ? 1 : 0);
+
+ localS.m_Sort[i].by = (index >= 0 && index < m_IndexToSortKey.size()) ? m_IndexToSortKey[index] : -1;
+ localS.m_Sort[i].asc = (m_Options.getRadioChecked(m_hSortDir[i]) == 0);
+ }
+
+ localS.m_OwnNick = m_Options.getEditString(m_hNick );
+ localS.m_OutputVariables = m_Options.isItemChecked(m_hOutputVariables );
+ localS.m_OutputFile = m_Options.getEditString(m_hOutputFile );
+ localS.m_OutputExtraToFolder = m_Options.isItemChecked(m_hOutputExtraToFolder);
+ localS.m_OutputExtraFolder = m_Options.getEditString(m_hOutputExtraFolder );
+ localS.m_OverwriteAlways = m_Options.isItemChecked(m_hOverwriteAlways );
+ localS.m_AutoOpenOptions = m_Options.isItemChecked(m_hAutoOpenOptions );
+ localS.m_AutoOpenStartup = m_Options.isItemChecked(m_hAutoOpenStartup );
+ localS.m_AutoOpenMenu = m_Options.isItemChecked(m_hAutoOpenMenu );
+}
+
+void DlgOption::SubOutput::onChanged(HANDLE hItem)
+{
+ if (hItem == m_hSortBy[1])
+ {
+ m_Options.enableItem(m_hSortBy[2], m_Options.getComboSelected(m_hSortBy[1]) != 0);
+ }
+ else if (hItem == m_hCalcTotals || hItem == m_hOmitContacts)
+ {
+ m_Options.enableItem(m_hOmittedInTotals, m_Options.isItemChecked(m_hCalcTotals) && m_Options.isItemChecked(m_hOmitContacts));
+ }
+
+ getParent()->settingsChanged();
+}
diff --git a/plugins/HistoryStats/src/iconlib.cpp b/plugins/HistoryStats/src/iconlib.cpp
new file mode 100644
index 0000000000..3647205764
--- /dev/null
+++ b/plugins/HistoryStats/src/iconlib.cpp
@@ -0,0 +1,149 @@
+#include "_globals.h"
+#include "iconlib.h"
+
+#include "main.h"
+#include "resource.h"
+
+bool IconLib::m_bIcoLibAvailable = false;
+HANDLE IconLib::m_hHookSkin2IconsChanged = NULL;
+
+IconLib::IconInfo IconLib::m_IconInfo[] = {
+ { IDI_HISTORYSTATS , muA("main_menu") , NULL , I18N(muT("Create statistics (main menu)")) },
+ { IDI_HISTORYSTATS , muA("menu_show") , NULL , I18N(muT("Show statistics (main menu)")) },
+ { IDI_HISTORYSTATS , muA("menu_config") , NULL , I18N(muT("Configure... (main menu)")) },
+ { IDI_HISTORYSTATS , muA("contact_menu") , NULL , I18N(muT("Contact menu")) },
+ { IDI_EXCLUDE_NO , muA("exclude_no") , NULL , I18N(muT("Unexcluded contacts")) },
+ { IDI_EXCLUDE_YES , muA("exclude_yes") , NULL , I18N(muT("Excluded contacts")) },
+ { IDI_TREE_CHECK1 , muA("tree_check1") , I18N(muT("Options tree")), I18N(muT("Checkbox")) },
+ { IDI_TREE_CHECK2 , muA("tree_check2") , I18N(muT("Options tree")), I18N(muT("Checkbox (checked)")) },
+ { IDI_TREE_CHECK3 , muA("tree_check3") , I18N(muT("Options tree")), I18N(muT("Checkbox (disabled)")) },
+ { IDI_TREE_CHECK4 , muA("tree_check4") , I18N(muT("Options tree")), I18N(muT("Checkbox (checked & disabled)")) },
+ { IDI_TREE_RADIO1 , muA("tree_radio1") , I18N(muT("Options tree")), I18N(muT("Radio button")) },
+ { IDI_TREE_RADIO2 , muA("tree_radio2") , I18N(muT("Options tree")), I18N(muT("Radio button (checked)")) },
+ { IDI_TREE_RADIO3 , muA("tree_radio3") , I18N(muT("Options tree")), I18N(muT("Radio button (disabled)")) },
+ { IDI_TREE_RADIO4 , muA("tree_radio4") , I18N(muT("Options tree")), I18N(muT("Radio button (checked & disabled)")) },
+ { IDI_TREE_EDIT1 , muA("tree_edit1") , I18N(muT("Options tree")), I18N(muT("Edit control")) },
+ { IDI_TREE_EDIT2 , muA("tree_edit2") , I18N(muT("Options tree")), I18N(muT("Edit control (disabled)")) },
+ { IDI_TREE_COMBO1 , muA("tree_combo1") , I18N(muT("Options tree")), I18N(muT("Combo box")) },
+ { IDI_TREE_COMBO2 , muA("tree_combo2") , I18N(muT("Options tree")), I18N(muT("Combo box (disabled)")) },
+ { IDI_TREE_FOLDER1 , muA("tree_folder1") , I18N(muT("Options tree")), I18N(muT("Folder")) },
+ { IDI_TREE_FOLDER2 , muA("tree_folder2") , I18N(muT("Options tree")), I18N(muT("Folder (disabled)")) },
+ { IDI_TREE_BUTTON1 , muA("tree_button1") , I18N(muT("Options tree")), I18N(muT("Button")) },
+ { IDI_TREE_BUTTON2 , muA("tree_button2") , I18N(muT("Options tree")), I18N(muT("Button (disabled)")) },
+ { IDI_TREE_DATETIME1, muA("tree_datetime1"), I18N(muT("Options tree")), I18N(muT("Date/time picker")) },
+ { IDI_TREE_DATETIME2, muA("tree_datetime2"), I18N(muT("Options tree")), I18N(muT("Date/time picker (disabled)")) },
+};
+
+ext::string IconLib::m_Section;
+ext::a::string IconLib::m_IconName;
+IconLib::CallbackSet IconLib::m_Callbacks;
+
+int IconLib::handleCallbacks(WPARAM wParam, LPARAM lParam)
+{
+ citer_each_(CallbackSet, i, m_Callbacks)
+ {
+ (*i->first)(i->second);
+ }
+
+ return 0;
+}
+
+void IconLib::init()
+{
+ array_each_(i, m_IconInfo)
+ {
+ m_IconInfo[i].hIcon = NULL;
+ }
+
+ if (m_bIcoLibAvailable = mu::icolib::_available())
+ {
+ bool bIcoLibTested = false;
+
+ mu_ansi szModule[MAX_PATH];
+
+ GetModuleFileNameA(g_hInst, szModule, MAX_PATH);
+
+ m_Section = muT("HistoryStats");
+ m_IconName = muA("historystats_");
+
+ array_each_(i, m_IconInfo)
+ {
+ ext::string strSection = m_Section;
+
+ if (m_IconInfo[i].szSection)
+ {
+ strSection += muT("/");
+ strSection += i18n(m_IconInfo[i].szSection);
+ }
+
+ mu::icolib::addIcon(
+ strSection.c_str(),
+ i18n(m_IconInfo[i].szDescription),
+ (m_IconName + m_IconInfo[i].szIconName).c_str(),
+ szModule,
+ -m_IconInfo[i].wID);
+
+ if (!bIcoLibTested)
+ {
+ bIcoLibTested = true;
+
+ if (!getIcon(static_cast<IconIndex>(i)))
+ {
+ m_bIcoLibAvailable = false;
+
+ break;
+ }
+ }
+ }
+
+ m_hHookSkin2IconsChanged = HookEvent(ME_SKIN2_ICONSCHANGED, handleCallbacks);
+ }
+
+ if (!m_bIcoLibAvailable)
+ {
+ array_each_(i, m_IconInfo)
+ {
+ m_IconInfo[i].hIcon = reinterpret_cast<HICON>(LoadImage(
+ g_hInst,
+ MAKEINTRESOURCE(m_IconInfo[i].wID),
+ IMAGE_ICON,
+ OS::smIconCX(),
+ OS::smIconCY(),
+ 0));
+ }
+ }
+}
+
+void IconLib::registerCallback(CallbackProc callback, LPARAM lParam)
+{
+ m_Callbacks.insert(std::make_pair(callback, lParam));
+}
+
+void IconLib::unregisterCallback(CallbackProc callback, LPARAM lParam)
+{
+ m_Callbacks.erase(std::make_pair(callback, lParam));
+}
+
+void IconLib::uninit()
+{
+ array_each_(i, m_IconInfo)
+ {
+ if (m_IconInfo[i].hIcon)
+ {
+ DestroyIcon(m_IconInfo[i].hIcon);
+ m_IconInfo[i].hIcon = NULL;
+ }
+ }
+}
+
+HICON IconLib::getIcon(IconIndex index)
+{
+ if (m_bIcoLibAvailable)
+ {
+ return mu::icolib::getIcon((m_IconName + m_IconInfo[index].szIconName).c_str());
+ }
+ else
+ {
+ return m_IconInfo[index].hIcon;
+ }
+}
diff --git a/plugins/HistoryStats/src/iconlib.h b/plugins/HistoryStats/src/iconlib.h
new file mode 100644
index 0000000000..bc4ab3b1c7
--- /dev/null
+++ b/plugins/HistoryStats/src/iconlib.h
@@ -0,0 +1,74 @@
+#if !defined(HISTORYSTATS_GUARD_ICONLIB_H)
+#define HISTORYSTATS_GUARD_ICONLIB_H
+
+#include "_globals.h"
+
+
+#include <set>
+
+class IconLib
+ : private pattern::NotInstantiable<IconLib>
+{
+public:
+ enum IconIndex {
+ iiMenuCreateStatistics = 0,
+ iiMenuShowStatistics = 1,
+ iiMenuConfigure = 2,
+ iiContactMenu = 3,
+ iiExcludeNo = 4,
+ iiExcludeYes = 5,
+ iiTreeCheck1 = 6,
+ iiTreeCheck2 = 7,
+ iiTreeCheck3 = 8,
+ iiTreeCheck4 = 9,
+ iiTreeRadio1 = 10,
+ iiTreeRadio2 = 11,
+ iiTreeRadio3 = 12,
+ iiTreeRadio4 = 13,
+ iiTreeEdit1 = 14,
+ iiTreeEdit2 = 15,
+ iiTreeCombo1 = 16,
+ iiTreeCombo2 = 17,
+ iiTreeFolder1 = 18,
+ iiTreeFolder2 = 19,
+ iiTreeButton1 = 20,
+ iiTreeButton2 = 21,
+ iiTreeDateTime1 = 22,
+ iiTreeDateTime2 = 23,
+ };
+
+ typedef void (*CallbackProc)(LPARAM lParam);
+
+private:
+ struct IconInfo
+ {
+ WORD wID;
+ mu_ansi* szIconName;
+ mu_text* szSection;
+ mu_text* szDescription;
+ HICON hIcon;
+ };
+
+ typedef std::pair<CallbackProc, LPARAM> CallbackPair;
+ typedef std::set<CallbackPair> CallbackSet;
+
+private:
+ static bool m_bIcoLibAvailable;
+ static HANDLE m_hHookSkin2IconsChanged;
+ static IconInfo m_IconInfo[];
+ static ext::string m_Section;
+ static ext::a::string m_IconName;
+ static CallbackSet m_Callbacks;
+
+private:
+ static int handleCallbacks(WPARAM wParam, LPARAM lParam);
+
+public:
+ static void init();
+ static void uninit();
+ static void registerCallback(CallbackProc callback, LPARAM lParam);
+ static void unregisterCallback(CallbackProc callback, LPARAM lParam);
+ static HICON getIcon(IconIndex index);
+};
+
+#endif // HISTORYSTATS_GUARD_ICONLIB_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/inout.h b/plugins/HistoryStats/src/inout.h
new file mode 100644
index 0000000000..c6e099169d
--- /dev/null
+++ b/plugins/HistoryStats/src/inout.h
@@ -0,0 +1,50 @@
+#if !defined(HISTORYSTATS_GUARD_INOUT_H)
+#define HISTORYSTATS_GUARD_INOUT_H
+
+class InOut
+{
+public:
+ int in;
+ int out;
+
+public:
+ InOut()
+ : in(0), out(0)
+ {
+ }
+
+ InOut(int initIn, int initOut)
+ : in(initIn), out(initOut)
+ {
+ }
+
+ InOut& operator +=(const InOut& other)
+ {
+ in += other.in;
+ out += other.out;
+
+ return *this;
+ }
+
+ int total() const
+ {
+ return in + out;
+ }
+
+ int operator <(const InOut& other) const
+ {
+ return total() < other.total();
+ }
+
+ int operator >(const InOut& other) const
+ {
+ return total() > other.total();
+ }
+
+ int operator !=(const InOut& other) const
+ {
+ return total() != other.total();
+ }
+};
+
+#endif // HISTORYSTATS_GUARD_INOUT_H
diff --git a/plugins/HistoryStats/src/main.cpp b/plugins/HistoryStats/src/main.cpp
new file mode 100644
index 0000000000..509d5f53a1
--- /dev/null
+++ b/plugins/HistoryStats/src/main.cpp
@@ -0,0 +1,549 @@
+#include "_globals.h"
+#include "main.h"
+
+#include <clocale>
+
+#include "dlgoption.h"
+#include "resource.h"
+#include "column.h"
+#include "bandctrlimpl.h"
+#include "optionsctrlimpl.h"
+#include "themeapi.h"
+#include "iconlib.h"
+#include "dlgconfigure.h"
+
+HINSTANCE g_hInst;
+int hLangpack;
+
+static const int g_pluginFileListID = 2535;
+
+const PLUGININFOEX g_pluginInfoEx = {
+ /* .cbSize = */ sizeof(PLUGININFOEX),
+ /* .shortName = */ muA("HistoryStats"),
+ /* .version = */ PLUGIN_MAKE_VERSION(0, 1, 5, 3),
+ /* .description = */ muA("Creates nice statistics using your message history.\r\n")
+ muA("(Requires Miranda IM ") MU_DO_BOTH(muA("0.6.7"), muA("0.6.7 Unicode")) muA(" or above.)"),
+ /* .author = */ muA("Martin Afanasjew"),
+ /* .authorEmail = */ muA("miranda@dark-passage.de"),
+ /* .copyright = */ muA("2005-2007 by Martin Afanasjew (see README for further credits)"),
+ /* .homepage = */ muA("http://addons.miranda-im.org/details.php?action=viewfile&id=") MU_DO_BOTH(muA("2534"), muA("2535")),
+ /* .flags = */ UNICODE_AWARE,
+ /* .uuid = */ { 0xf184f5a0, 0xc198, 0x4454, { 0xa9, 0xb4, 0xf6, 0xe2, 0xfd, 0x53, 0x41, 0x33 } },
+};
+
+SettingsSerializer* g_pSettings = NULL;
+
+bool g_bMainMenuExists = false;
+bool g_bContactMenuExists = false;
+bool g_bExcludeLock = false;
+bool g_bConfigureLock = false;
+
+static HANDLE g_hMenuCreateStatistics = NULL;
+static HANDLE g_hMenuShowStatistics = NULL;
+static HANDLE g_hMenuConfigure = NULL;
+static HANDLE g_hMenuToggleExclude = NULL;
+
+#if defined(HISTORYSTATS_HISTORYCOPY)
+static HANDLE g_hMenuHistoryCopy = NULL;
+static HANDLE g_hMenuHistoryPaste = NULL;
+
+static HANDLE g_hHistoryCopyContact = NULL;
+#endif
+
+/*
+ * services (see m_historystats.h for details)
+ */
+
+static INT_PTR SvcIsExcluded(WPARAM hContact, LPARAM lParam)
+{
+ if (hContact) {
+ MirandaSettings db;
+
+ db.setContact(hContact);
+ db.setModule(con::ModHistoryStats);
+
+ return db.readBool(con::SettExclude, false) ? 1 : 0;
+ }
+
+ return 0;
+}
+
+static INT_PTR SvcSetExclude(WPARAM hContact, LPARAM lParam)
+{
+ if (hContact) {
+ MirandaSettings db;
+
+ db.setContact(hContact);
+ db.setModule(con::ModHistoryStats);
+
+ if (db.readBool(con::SettExclude, false))
+ {
+ if (!lParam)
+ {
+ db.delSetting(con::SettExclude);
+ }
+ }
+ else
+ {
+ if (lParam)
+ {
+ db.writeBool(con::SettExclude, true);
+ }
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * global menu stuff
+ */
+
+static void MenuIconsChanged(LPARAM lParam)
+{
+ if (g_hMenuCreateStatistics)
+ {
+ mu::clist::modifyMenuItem(g_hMenuCreateStatistics, CMIM_ICON, NULL, 0, IconLib::getIcon(IconLib::iiMenuCreateStatistics));
+ }
+
+ if (g_hMenuShowStatistics)
+ {
+ mu::clist::modifyMenuItem(g_hMenuShowStatistics, CMIM_ICON, NULL, 0, IconLib::getIcon(IconLib::iiMenuShowStatistics));
+ }
+
+ if (g_hMenuConfigure)
+ {
+ mu::clist::modifyMenuItem(g_hMenuConfigure, CMIM_ICON, NULL, 0, IconLib::getIcon(IconLib::iiMenuConfigure));
+ }
+
+ if (g_hMenuToggleExclude)
+ {
+ mu::clist::modifyMenuItem(g_hMenuToggleExclude, CMIM_ICON, NULL, 0, IconLib::getIcon(IconLib::iiContactMenu));
+ }
+}
+
+/*
+ * main menu related stuff
+ */
+
+static INT_PTR MenuCreateStatistics(WPARAM wParam, LPARAM lParam)
+{
+ Statistic::run(*g_pSettings, Statistic::fromMenu, g_hInst);
+ return 0;
+}
+
+static INT_PTR MenuShowStatistics(WPARAM wParam, LPARAM lParam)
+{
+ if (g_pSettings->canShowStatistics())
+ {
+ g_pSettings->showStatistics();
+ }
+ else
+ {
+ MessageBox(
+ 0,
+ i18n(muT("The statistics can't be found. Either you never created them or the last created statistics were moved to a different location and can't be found anymore.")),
+ i18n(muT("HistoryStats - Warning")),
+ MB_ICONWARNING | MB_OK);
+ }
+
+ return 0;
+}
+
+static INT_PTR MenuConfigure(WPARAM wParam, LPARAM lParam)
+{
+ DlgConfigure::showModal();
+ return 0;
+}
+
+void AddMainMenu()
+{
+ if (!g_pSettings->m_ShowMainMenu || g_bMainMenuExists)
+ {
+ return;
+ }
+
+ g_bMainMenuExists = true;
+
+ bool bInPopup = g_pSettings->m_ShowMainMenuSub;
+
+ CreateServiceFunction(con::SvcCreateStatistics, MenuCreateStatistics);
+ CreateServiceFunction(con::SvcShowStatistics, MenuShowStatistics);
+ CreateServiceFunction(con::SvcConfigure, MenuConfigure);
+
+ g_hMenuCreateStatistics = mu::clist::addMainMenuItem(
+ I18N(muT("Create statistics")), // MEMO: implicit translation
+ 0,
+ 1910000000,
+ IconLib::getIcon(IconLib::iiMenuCreateStatistics),
+ con::SvcCreateStatistics,
+ bInPopup ? I18N(muT("Statistics")) : NULL, // MEMO: implicit translation
+ bInPopup ? 1910000000 : 0);
+
+ g_hMenuShowStatistics = mu::clist::addMainMenuItem(
+ I18N(muT("Show statistics")), // MEMO: implicit translation
+ 0,
+ 1910000001,
+ IconLib::getIcon(IconLib::iiMenuShowStatistics),
+ con::SvcShowStatistics,
+ bInPopup ? I18N(muT("Statistics")) : NULL, // MEMO: implicit translation
+ bInPopup ? 1910000000 : 0);
+
+ g_hMenuConfigure = mu::clist::addMainMenuItem(
+ bInPopup ? I18N(muT("Configure...")) : I18N(muT("Configure statistics...")), // MEMO: implicit translation
+ 0,
+ 1910000002,
+ IconLib::getIcon(IconLib::iiMenuConfigure),
+ con::SvcConfigure,
+ bInPopup ? I18N(muT("Statistics")) : NULL, // MEMO: implicit translation
+ bInPopup ? 1910000000 : 0);
+}
+
+/*
+ * contact menu related stuff
+ */
+
+static INT_PTR MenuToggleExclude(WPARAM hContact, LPARAM lParam)
+{
+ if (hContact) {
+ MirandaSettings db;
+
+ db.setContact(hContact);
+ db.setModule(con::ModHistoryStats);
+
+ if (db.readBool(con::SettExclude, false))
+ db.delSetting(con::SettExclude);
+ else
+ db.writeBool(con::SettExclude, true);
+ }
+
+ return 0;
+}
+
+#if defined(HISTORYSTATS_HISTORYCOPY)
+static INT_PTR MenuHistoryCopy(WPARAM hContact, LPARAM lParam)
+{
+ if (hContact)
+ g_hHistoryCopyContact = hContact;
+ return 0;
+}
+
+static INT_PTR MenuHistoryPaste(WPARAM wParam, LPARAM lParam)
+{
+ HANDLE hTarget = reinterpret_cast<HANDLE>(wParam);
+
+ if (!hTarget)
+ {
+ return 0;
+ }
+
+ // ask user if this is really what he wants
+ ext::string strConfirm = ext::str(ext::kformat(i18n(muT("You're going to copy the complete history of #{source_name} (#{source_proto}) to #{target_name} (#{target_proto}). Afterwards, the target history will contain entries from both histories. There is no way to revert this operation. Be careful! This is a rather big operation and has the potential to damage your database. Be sure to have a backup of this database before performing this operation.\r\n\r\nAre you sure you would like to continue?")))
+ % muT("#{source_name}") * mu::clist::getContactDisplayName(g_hHistoryCopyContact)
+ % muT("#{source_proto}") * utils::fromA(GetContactProto(g_hHistoryCopyContact))
+ % muT("#{target_name}") * mu::clist::getContactDisplayName(hTarget)
+ % muT("#{target_proto}") * utils::fromA(GetContactProto(hTarget)));
+
+ if (MessageBox(0, strConfirm.c_str(), i18n(muT("HistoryStats - Confirm")), MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES)
+ {
+ return 0;
+ }
+
+ // turn off safety mode
+ mu::db::setSafetyMode(false);
+
+ // copy history
+ DWORD dwCountSuccess = 0, dwCountFailRead = 0, dwCountFailAdd = 0;
+ DBEVENTINFO dbe;
+ int blobBuffer = 4096;
+ HANDLE hEvent = mu::db_event::findFirst(g_hHistoryCopyContact);
+
+ ZeroMemory(&dbe, sizeof(dbe));
+ dbe.cbSize = sizeof(dbe);
+ dbe.pBlob = reinterpret_cast<BYTE*>(malloc(blobBuffer));
+
+ while (hEvent)
+ {
+ dbe.cbBlob = db_event_getBlobSize(hEvent);
+
+ if (blobBuffer < dbe.cbBlob)
+ {
+ blobBuffer = 4096 * ((4095 + dbe.cbBlob) / 4096);
+ dbe.pBlob = reinterpret_cast<BYTE*>(realloc(dbe.pBlob, blobBuffer));
+ }
+
+ if (db_event_get(hEvent, &dbe) == 0) {
+ ++dwCountSuccess;
+
+ // clear "first" flag
+ dbe.flags &= ~DBEF_FIRST;
+
+ if (mu::db_event::add(hTarget, &dbe) == NULL)
+ ++dwCountFailAdd;
+ }
+ else ++dwCountFailRead;
+
+ hEvent = db_event_findNext(hEvent);
+ }
+
+ free(dbe.pBlob);
+
+ // turn safety mode back on
+ mu::db::setSafetyMode(true);
+
+ // output summary
+ ext::string strSummary = ext::str(ext::kformat(i18n(muT("Successfully read #{success} events of which #{fail_add} couldn't be added to the target history. #{fail} events couldn't be read from the source history.")))
+ % muT("#{success}") * dwCountSuccess
+ % muT("#{fail}") * dwCountFailRead
+ % muT("#{fail_add}") * dwCountFailAdd);
+
+ MessageBox(0, strSummary.c_str(), i18n(muT("HistoryStats - Information")), MB_ICONINFORMATION);
+
+ g_hHistoryCopyContact = NULL;
+
+ return 0;
+}
+#endif
+
+static int EventPreBuildContactMenu(WPARAM hContact, LPARAM lParam)
+{
+ if (hContact)
+ {
+ const mu_ansi* szProto = GetContactProto(hContact);
+
+ if ((!g_pSettings->m_ShowContactMenuPseudo && (!szProto || !(mu::protosvc::getCaps(szProto, PFLAGNUM_2) & ~mu::protosvc::getCaps(szProto, PFLAGNUM_5)))) ||
+ g_pSettings->m_HideContactMenuProtos.find(szProto) != g_pSettings->m_HideContactMenuProtos.end())
+ {
+ mu::clist::modifyMenuItem(g_hMenuToggleExclude, CMIM_FLAGS, NULL, CMIF_HIDDEN);
+ }
+ else
+ {
+ MirandaSettings db;
+
+ db.setContact(hContact);
+ db.setModule(con::ModHistoryStats);
+
+ int menuState = db.readBool(con::SettExclude, false) ? CMIF_CHECKED : 0;
+
+ // avoid collision with options page
+ if (g_bExcludeLock)
+ {
+ menuState |= CMIF_GRAYED;
+ }
+
+ // set menu state
+ mu::clist::modifyMenuItem(g_hMenuToggleExclude, CMIM_FLAGS, NULL, menuState);
+ }
+
+#if defined(HISTORYSTATS_HISTORYCOPY)
+ int menuStateCopy = (g_hHistoryCopyContact && g_hHistoryCopyContact != hContact) ? 0 : CMIF_GRAYED;
+
+ mu::clist::modifyMenuItem(g_hMenuHistoryPaste, CMIM_FLAGS, NULL, menuStateCopy);
+#endif
+ }
+
+ return 0;
+}
+
+void AddContactMenu()
+{
+ if (!g_pSettings->m_ShowContactMenu || g_bContactMenuExists)
+ return;
+
+ g_bContactMenuExists = true;
+
+ CreateServiceFunction(con::SvcToggleExclude, MenuToggleExclude);
+
+ g_hMenuToggleExclude = mu::clist::addContactMenuItem(
+ I18N(muT("Exclude from statistics")), // MEMO: implicit translation
+ 0,
+ 800000,
+ IconLib::getIcon(IconLib::iiContactMenu),
+ con::SvcToggleExclude);
+
+#if defined(HISTORYSTATS_HISTORYCOPY)
+ CreateServiceFunction(con::SvcHistoryCopy, MenuHistoryCopy);
+ CreateServiceFunction(con::SvcHistoryPaste, MenuHistoryPaste);
+
+ g_hMenuHistoryCopy = mu::clist::addContactMenuItem(
+ I18N(muT("Copy history")), // MEMO: implicit translation
+ 0,
+ 800001,
+ NULL,
+ con::SvcHistoryCopy);
+
+ g_hMenuHistoryPaste = mu::clist::addContactMenuItem(
+ I18N(muT("Paste history...")), // MEMO: implicit translation
+ 0,
+ 800002,
+ NULL,
+ con::SvcHistoryPaste);
+#endif
+
+ HookEvent(ME_CLIST_PREBUILDCONTACTMENU, EventPreBuildContactMenu);
+}
+
+/*
+ * options integration
+ */
+
+static int EventOptInitialise(WPARAM wParam, LPARAM lParam)
+{
+ mu::opt::addPage(
+ wParam,
+ i18n(muT("History")),
+ i18n(muT("Statistics")),
+ NULL,
+ DlgOption::staticDlgProc,
+ MAKEINTRESOURCEA(IDD_OPTIONS),
+ g_hInst);
+
+ return 0;
+}
+
+/*
+ * second initialization phase
+ */
+
+static int EventModulesLoaded(WPARAM wParam, LPARAM lParam)
+{
+ // register all known columns
+ Column::registerColumns();
+
+ // read settings
+ g_pSettings = new SettingsSerializer(con::ModHistoryStats);
+ g_pSettings->readFromDB();
+
+ // integrate into options dialog
+ HookEvent(ME_OPT_INITIALISE, EventOptInitialise);
+
+ // integrate with icolib
+ IconLib::init();
+ IconLib::registerCallback(MenuIconsChanged, 0);
+
+ // integrate into main/contact menu, if selected
+ AddMainMenu();
+ AddContactMenu();
+
+ // create statistics on startup, if activated
+ if (g_pSettings->m_OnStartup)
+ {
+ Statistic::run(*g_pSettings, Statistic::fromStartup, g_hInst);
+ }
+
+ return 0;
+}
+
+/*
+ * external interface
+ */
+
+extern "C" BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
+{
+ switch (fdwReason)
+ {
+ case DLL_PROCESS_ATTACH:
+ {
+ DisableThreadLibraryCalls(hinstDLL);
+ g_hInst = hinstDLL;
+
+#if defined(_DEBUG)
+ // dump memory leak report at end of program
+ _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
+#endif
+ }
+ break;
+ }
+
+ return TRUE;
+}
+
+extern "C" __declspec(dllexport) const PLUGININFOEX* MirandaPluginInfoEx(DWORD mirandaVersion)
+{
+ OutputDebugString(muT("HistoryStats: MirandaPluginInfoEx() was called.\n"));
+
+ // MEMO: (don't) fail, if version is below minimum
+ return &g_pluginInfoEx;
+}
+
+extern "C" __declspec(dllexport) int Load()
+{
+ // init themeing api
+ ThemeAPI::init();
+
+ // init COM, needed for GUID generation
+ CoInitialize(NULL);
+
+ // register our own window classes
+ if (!BandCtrlImpl::registerClass() || !OptionsCtrlImpl::registerClass())
+ {
+ MessageBox(
+ 0,
+ muT("Failed to register a required window class. Can't continue loading plugin."),
+ muT("HistoryStats - Error"),
+ MB_OK | MB_ICONERROR);
+
+ return 1;
+ }
+
+ // load "mu" system (includes version check)
+ if (!mu::load())
+ {
+ MessageBox(
+ 0,
+ muT("This version of HistoryStats isn't compatible with your Miranda IM ")
+ muT("version. Possibly, your Miranda IM is outdated or you are trying to ")
+ muT("use the Unicode version with a non-Unicode Miranda IM.\r\n\r\n")
+ muT("Please go to the plugin's homepage and check the requirements."),
+ muT("HistoryStats - Error"),
+ MB_OK | MB_ICONERROR);
+
+ return 1;
+ }
+
+ // use system's locale to format date/time/numbers
+ Locale::init();
+
+ // load rtfconv.dll if available
+ RTFFilter::init();
+
+ // init global variables
+ g_bMainMenuExists = false;
+ g_bContactMenuExists = false;
+
+ // register provided services
+ CreateServiceFunction(MS_HISTORYSTATS_ISEXCLUDED, SvcIsExcluded);
+ CreateServiceFunction(MS_HISTORYSTATS_SETEXCLUDE, SvcSetExclude);
+
+ // hook "modules loaded" to perform further initialization
+ HookEvent(ME_SYSTEM_MODULESLOADED, EventModulesLoaded);
+
+ return 0;
+}
+
+extern "C" __declspec(dllexport) int Unload()
+{
+ // free global settings object
+ delete g_pSettings;
+
+ // uninit iconlib
+ IconLib::uninit();
+
+ // free rtfconv.dll if loaded
+ RTFFilter::uninit();
+
+ // unload "mu" system
+ mu::unload();
+
+ // unregister our own window classes
+ OptionsCtrlImpl::unregisterClass();
+ BandCtrlImpl::unregisterClass();
+
+ // uninit COM, needed for GUID generation
+ CoUninitialize();
+
+ // free themeing api
+ ThemeAPI::uninit();
+
+ return 0;
+}
diff --git a/plugins/HistoryStats/src/main.h b/plugins/HistoryStats/src/main.h
new file mode 100644
index 0000000000..cc0292adb9
--- /dev/null
+++ b/plugins/HistoryStats/src/main.h
@@ -0,0 +1,33 @@
+#if !defined(HISTORYSTATS_GUARD_MAIN_H)
+#define HISTORYSTATS_GUARD_MAIN_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include "settingsserializer.h"
+#include "statistic.h"
+
+extern HINSTANCE g_hInst;
+
+extern const PLUGININFOEX g_pluginInfoEx;
+
+extern SettingsSerializer* g_pSettings;
+
+extern bool g_bMainMenuExists;
+extern bool g_bContactMenuExists;
+extern bool g_bExcludeLock;
+extern bool g_bConfigureLock;
+
+void AddMainMenu();
+void AddContactMenu();
+
+// shortcut for filling std::vector
+template <typename T_>
+std::vector<T_>& operator <<(std::vector<T_>& container, const T_& value)
+{
+ container.push_back(value);
+
+ return container;
+}
+
+#endif // HISTORYSTATS_GUARD_MAIN_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/message.cpp b/plugins/HistoryStats/src/message.cpp
new file mode 100644
index 0000000000..b72f92bfda
--- /dev/null
+++ b/plugins/HistoryStats/src/message.cpp
@@ -0,0 +1,334 @@
+#include "_globals.h"
+#include "message.h"
+
+#include <algorithm>
+
+/*
+ * Message
+ */
+
+void Message::makeRawAvailable()
+{
+ do
+ {
+#if defined(MU_WIDE)
+ if (m_Available & PtrIsNonT)
+ {
+ m_Raw = utils::fromA(ext::a::string(reinterpret_cast<const mu_ansi*>(m_RawSource), m_nLength));
+ m_Available |= Raw;
+
+ break;
+ }
+#endif // MU_WIDE
+
+ if (m_Available & PtrIsUTF8)
+ {
+ m_Raw = utils::fromUTF8(reinterpret_cast<const mu_ansi*>(m_RawSource));
+ m_Available |= Raw;
+
+ break;
+ }
+
+ m_Raw.assign(reinterpret_cast<const mu_text*>(m_RawSource), m_nLength);
+ m_Available |= Raw;
+ } while(false);
+
+ if (m_bStripRawRTF)
+ {
+ stripRawRTF();
+ }
+
+ if (m_bStripBBCodes)
+ {
+ stripBBCodes();
+ }
+}
+
+void Message::stripRawRTF()
+{
+ if (m_Raw.substr(0, 6) == muT("{\\rtf1"))
+ {
+ m_Raw = RTFFilter::filter(m_Raw);
+ }
+}
+
+void Message::stripBBCodes()
+{
+ static const mu_text* szSimpleBBCodes[][2] = {
+ { muT("[b]"), muT("[/b]") },
+ { muT("[u]"), muT("[/u]") },
+ { muT("[i]"), muT("[/i]") },
+ { muT("[s]"), muT("[/s]") },
+ };
+
+ static const mu_text* szParamBBCodes[][2] = {
+ { muT("[url=") , muT("[/url]") },
+ { muT("[color="), muT("[/color]") },
+ };
+
+ // convert raw string to lower case
+ ext::string strRawLC = utils::toLowerCase(m_Raw);
+
+ // remove simple BBcodes
+ array_each_(i, szSimpleBBCodes)
+ {
+ const mu_text* szOpenTag = szSimpleBBCodes[i][0];
+ const mu_text* szCloseTag = szSimpleBBCodes[i][1];
+
+ int lenOpen = ext::strfunc::len(szOpenTag);
+ int lenClose = ext::strfunc::len(szCloseTag);
+
+ ext::string::size_type posOpen = 0;
+ ext::string::size_type posClose = 0;
+
+ while (true)
+ {
+ if ((posOpen = strRawLC.find(szOpenTag, posOpen)) == ext::string::npos)
+ {
+ break;
+ }
+
+ if ((posClose = strRawLC.find(szCloseTag, posOpen + lenOpen)) == ext::string::npos)
+ {
+ break;
+ }
+
+ strRawLC.erase(posOpen, lenOpen);
+ strRawLC.erase(posClose - lenOpen, lenClose);
+
+ // fix real string
+ m_Raw.erase(posOpen, lenOpen);
+ m_Raw.erase(posClose - lenOpen, lenClose);
+ }
+ }
+
+ // remove BBcodes with parameters
+ array_each_(i, szParamBBCodes)
+ {
+ const mu_text* szOpenTag = szParamBBCodes[i][0];
+ const mu_text* szCloseTag = szParamBBCodes[i][1];
+
+ int lenOpen = ext::strfunc::len(szOpenTag);
+ int lenClose = ext::strfunc::len(szCloseTag);
+
+ ext::string::size_type posOpen = 0;
+ ext::string::size_type posOpen2 = 0;
+ ext::string::size_type posClose = 0;
+
+ while (true)
+ {
+ if ((posOpen = strRawLC.find(szOpenTag, posOpen)) == ext::string::npos)
+ {
+ break;
+ }
+
+ if ((posOpen2 = strRawLC.find(muC(']'), posOpen + lenOpen)) == ext::string::npos)
+ {
+ break;
+ }
+
+ if ((posClose = strRawLC.find(szCloseTag, posOpen2 + 1)) == ext::string::npos)
+ {
+ break;
+ }
+
+ strRawLC.erase(posOpen, posOpen2 - posOpen + 1);
+ strRawLC.erase(posClose - posOpen2 + posOpen - 1, lenClose);
+
+ // fix real string
+ m_Raw.erase(posOpen, posOpen2 - posOpen + 1);
+ m_Raw.erase(posClose - posOpen2 + posOpen - 1, lenClose);
+ }
+ }
+}
+
+void Message::filterLinks()
+{
+ static const mu_text* szSpaces = muT(" \r\r\n");
+ static const mu_text* szPrefixes = muT("([{<:\"'");
+ static const mu_text* szSuffixes = muT(".,:;!?)]}>\"'");
+ static const mu_text* szValidProtocol = muT("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ static const mu_text* szValidHost = muT(".-abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+
+ // init with raw text
+ m_WithoutLinks = getRaw();
+
+ ext::string& msg = m_WithoutLinks;
+ ext::string::size_type pos = -1;
+
+ // detect: protocol://[user[:password]@]host[/path]
+ while (true)
+ {
+ if ((pos = msg.find(muT("://"), pos + 1)) == ext::string::npos)
+ {
+ break;
+ }
+
+ // find start of URL
+ ext::string::size_type pos_proto = msg.find_last_not_of(szValidProtocol, pos - 1);
+
+ (pos_proto == ext::string::npos) ? pos_proto = 0 : ++pos_proto;
+
+ if (pos_proto < pos)
+ {
+ // find end of URL
+ ext::string::size_type pos_last = msg.find_first_of(szSpaces, pos + 3);
+
+ (pos_last == ext::string::npos) ? pos_last = msg.length() - 1 : --pos_last;
+
+ // filter suffixes (punctuation, parentheses, ...)
+ if (ext::strfunc::chr(szSuffixes, msg[pos_last]))
+ {
+ --pos_last;
+ }
+
+ // find slash: for host name validation
+ ext::string::size_type pos_slash = msg.find(muC('/'), pos + 3);
+
+ if (pos_slash == ext::string::npos || pos_slash > pos_last)
+ {
+ pos_slash = pos_last + 1;
+ }
+
+ // find at: for host name validation
+ ext::string::size_type pos_at = msg.find(muC('@'), pos + 3);
+
+ if (pos_at == ext::string::npos || pos_at > pos_slash)
+ {
+ pos_at = pos + 2;
+ }
+
+ // check for valid host (x.x)
+ if (pos_slash - pos_at > 3)
+ {
+ ext::string::size_type pos_invalid = msg.find_first_not_of(szValidHost, pos_at + 1);
+
+ if (pos_invalid == ext::string::npos || pos_invalid >= pos_slash)
+ {
+ if (std::count(msg.begin() + pos_at + 1, msg.begin() + pos_slash, muC('.')) >= 1)
+ {
+ ext::string link = msg.substr(pos_proto, pos_last - pos_proto + 1);
+
+ // remove extracted link from message text
+ msg.erase(pos_proto, link.length());
+ pos = pos_last - link.length();
+
+ // TODO: put link in list
+ }
+ }
+ }
+ }
+ }
+
+ // detect: www.host[/path]
+ pos = -1;
+
+ while (true)
+ {
+ if ((pos = msg.find(muT("www."), pos + 1)) == ext::string::npos)
+ {
+ break;
+ }
+
+ // find end of URL
+ ext::string::size_type pos_last = msg.find_first_of(szSpaces, pos + 4);
+
+ (pos_last == ext::string::npos) ? pos_last = msg.length() - 1 : --pos_last;
+
+ // filter suffixes (punctuation, parentheses, ...)
+ if (ext::strfunc::chr(szSuffixes, msg[pos_last]))
+ {
+ --pos_last;
+ }
+
+ // find slash: for host name validation
+ ext::string::size_type pos_slash = msg.find(muC('/'), pos + 4);
+
+ if (pos_slash == ext::string::npos || pos_slash > pos_last)
+ {
+ pos_slash = pos_last + 1;
+ }
+
+ // find at: for host name validation
+ ext::string::size_type pos_at = pos + 3;
+
+ // check for valid host (x.x)
+ if (pos_slash - pos_at > 3)
+ {
+ ext::string::size_type pos_invalid = msg.find_first_not_of(szValidHost, pos_at + 1);
+
+ if (pos_invalid == ext::string::npos || pos_invalid >= pos_slash)
+ {
+ if (std::count(msg.begin() + pos_at + 1, msg.begin() + pos_slash, muC('.')) >= 1)
+ {
+ ext::string link = muT("http://") + msg.substr(pos, pos_last - pos + 1);
+
+ // remove extracted link from message text
+ msg.erase(pos, link.length() - 7);
+ pos = pos_last - (link.length() - 7);
+
+ // TODO: put link in list
+ }
+ }
+ }
+ }
+
+ // detect: user@host
+ pos = -1;
+
+ while (true)
+ {
+ if ((pos = msg.find(muC('@'), pos + 1)) == ext::string::npos)
+ {
+ break;
+ }
+
+ if (pos > 0 && pos < msg.length() - 1)
+ {
+ // find end of address
+ ext::string::size_type pos_last = msg.find_first_not_of(szValidHost, pos + 1);
+
+ (pos_last == ext::string::npos) ? pos_last = msg.length() - 1 : --pos_last;
+
+ // filter suffixes (punctuation, parentheses, ...)
+ if (ext::strfunc::chr(szSuffixes, msg[pos_last]))
+ {
+ --pos_last;
+ }
+
+ // find start of address
+ ext::string::size_type pos_first = msg.find_last_of(szSpaces, pos - 1);
+
+ (pos_first == ext::string::npos) ? pos_first = 0 : ++pos_first;
+
+ // filter prefixes (punctuation, parentheses, ...)
+ if (ext::strfunc::chr(szPrefixes, msg[pos_first]))
+ {
+ ++pos_first;
+ }
+
+ // check for valid host (x.x)
+ if (pos_first < pos && pos_last - pos >= 3)
+ {
+ if (std::count(msg.begin() + pos + 1, msg.begin() + pos_last + 1, muC('.')) >= 1)
+ {
+ ext::string link = msg.substr(pos_first, pos_last - pos_first + 1);
+
+ // remove extracted link from message text
+ msg.erase(pos_first, link.length());
+ pos = pos_last - (link.length());
+
+ // prepend "mailto:" if missing
+ if (link.substr(0, 7) != muT("mailto:"))
+ {
+ link.insert(0, muT("mailto:"));
+ }
+
+ // TODO: put link in list
+ }
+ }
+ }
+ }
+
+ m_Available |= WithoutLinks;
+}
diff --git a/plugins/HistoryStats/src/message.h b/plugins/HistoryStats/src/message.h
new file mode 100644
index 0000000000..fe239c5be4
--- /dev/null
+++ b/plugins/HistoryStats/src/message.h
@@ -0,0 +1,121 @@
+#if !defined(HISTORYSTATS_GUARD_MESSAGE_H)
+#define HISTORYSTATS_GUARD_MESSAGE_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include "utils.h"
+
+/*
+ * Message
+ */
+
+class Message
+ : private pattern::NotCopyable<Message>
+{
+private:
+ enum Flags {
+ // internal
+ Raw = 0x01,
+ WithoutLinks = 0x02,
+
+ // very internal
+ PtrIsNonT = 0x10,
+ PtrIsUTF8 = 0x20,
+ };
+
+private:
+ bool m_bOutgoing;
+ DWORD m_Timestamp;
+ ext::string::size_type m_nLength;
+ const void* m_RawSource;
+ int m_Available;
+ ext::string m_Raw;
+ ext::string m_WithoutLinks;
+ bool m_bStripRawRTF;
+ bool m_bStripBBCodes;
+
+private:
+ void makeRawAvailable();
+ void stripRawRTF();
+ void stripBBCodes();
+ void filterLinks();
+
+public:
+ explicit Message(bool bStripRawRTF, bool bStripBBCodes)
+ : m_RawSource(NULL)
+ , m_Available(0)
+ , m_bStripRawRTF(bStripRawRTF)
+ , m_bStripBBCodes(bStripBBCodes)
+ {
+ }
+
+ // assigning data
+ void assignInfo(bool bOutgoing, DWORD localTimestamp)
+ {
+ m_bOutgoing = bOutgoing;
+ m_Timestamp = localTimestamp;
+ }
+
+ void assignText(const mu_text* msg, ext::string::size_type len)
+ {
+ m_RawSource = msg;
+ m_nLength = len;
+ m_Available = 0;
+ }
+
+#if defined(MU_WIDE)
+ void assignText(const mu_ansi* msg, ext::string::size_type len)
+ {
+ m_RawSource = msg;
+ m_nLength = len;
+ m_Available = PtrIsNonT;
+ }
+#endif // MU_WIDE
+
+ void assignTextFromUTF8(const mu_ansi* msg, ext::string::size_type len)
+ {
+ m_RawSource = msg;
+ m_nLength = len;
+ m_Available = PtrIsUTF8;
+ }
+
+ // retrieving always available data
+ bool isOutgoing()
+ {
+ return m_bOutgoing;
+ }
+
+ DWORD getTimestamp()
+ {
+ return m_Timestamp;
+ }
+
+ // retrieving on-demand data
+ ext::string::size_type getLength()
+ {
+ return (!m_bStripBBCodes && !m_bStripRawRTF) ? m_nLength : getRaw().length();
+ }
+
+ const ext::string& getRaw()
+ {
+ if (!(m_Available & Raw))
+ {
+ makeRawAvailable();
+ }
+
+ return m_Raw;
+ }
+
+ const ext::string& getWithoutLinks()
+ {
+ if (!(m_Available & WithoutLinks))
+ {
+ filterLinks();
+ }
+
+ return m_WithoutLinks;
+ }
+};
+
+#endif // HISTORYSTATS_GUARD_MESSAGE_H
diff --git a/plugins/HistoryStats/src/mirandacontact.cpp b/plugins/HistoryStats/src/mirandacontact.cpp
new file mode 100644
index 0000000000..367a36fb4e
--- /dev/null
+++ b/plugins/HistoryStats/src/mirandacontact.cpp
@@ -0,0 +1,383 @@
+#include "_globals.h"
+#include "mirandacontact.h"
+
+#include "_consts.h"
+
+#include "utils.h"
+#include "main.h"
+
+/*
+ * MirandaContact
+ */
+
+void MirandaContact::fetchSlot(int i)
+{
+ ContactInfo& ci = m_CIs[i];
+
+ if (!ci.hEvent)
+ {
+ free(ci.ei.dbe.pBlob);
+
+ m_CIs.erase(m_CIs.begin() + i);
+
+ return;
+ }
+
+ EventInfo& ei = ci.ei;
+
+ ei.hContact = ci.hContact;
+ ei.dbe.cbBlob = db_event_getBlobSize(ci.hEvent);
+ ei.dbe.cbSize = sizeof(ei.dbe);
+
+ if (ei.dbe.cbBlob > ei.nAllocated)
+ {
+ ei.nAllocated = ei.dbe.cbBlob;
+ ei.dbe.pBlob = reinterpret_cast<PBYTE>(realloc(ei.dbe.pBlob, ei.dbe.cbBlob + 1));
+ }
+
+ db_event_get(ci.hEvent, &ei.dbe);
+
+ stripMetaID(ei.dbe);
+
+ ci.hEvent = db_event_next(ci.hEvent);
+}
+
+void MirandaContact::stripMetaID(DBEVENTINFO& dbe)
+{
+ if (dbe.szModule == META_PROTO)
+ {
+ mu_ansi* pTextBegin = reinterpret_cast<mu_ansi*>(dbe.pBlob);
+
+ if (dbe.cbBlob >= 6 && !pTextBegin[dbe.cbBlob - 1])
+ {
+ mu_ansi* pIDEnd = pTextBegin + dbe.cbBlob - 1;
+ mu_ansi* pIDBegin = pIDEnd;
+ mu_ansi* pIDSep = NULL;
+
+ while (pIDBegin >= pTextBegin + 2 && *--pIDBegin)
+ {
+ if (*pIDBegin == muC('*'))
+ {
+ pIDSep = pIDBegin;
+ }
+ }
+
+ ++pIDBegin;
+
+ if (pIDSep && pIDBegin < pIDSep && !*(pIDBegin - 1))
+ {
+ dbe.cbBlob = pIDBegin - pTextBegin;
+ }
+ }
+ }
+}
+
+MirandaContact::MirandaContact(const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const SourceHandles& sources)
+ : m_strNick(strNick)
+ , m_strProtocol(strProtocol)
+ , m_strGroup(strGroup)
+ , m_Sources(sources)
+{
+}
+
+MirandaContact::~MirandaContact()
+{
+ endRead();
+}
+
+void MirandaContact::merge(const MirandaContact& other)
+{
+ if (m_strNick != other.m_strNick)
+ {
+ m_strNick = i18n(muT("(multiple)"));
+ }
+
+ if (m_strProtocol != other.m_strProtocol)
+ {
+ m_strProtocol = i18n(muT("(multiple)"));
+ }
+
+ if (m_strGroup != other.m_strGroup)
+ {
+ m_strGroup = i18n(muT("(multiple)"));
+ }
+
+ citer_each_(SourceHandles, i, other.m_Sources)
+ {
+ m_Sources.push_back(*i);
+ }
+}
+
+void MirandaContact::beginRead()
+{
+ // clean up first
+ endRead();
+
+ // allocate required data
+ m_CIs.resize(m_Sources.size());
+
+ for (int j = m_Sources.size() - 1; j >= 0; --j)
+ {
+ ContactInfo& ci = m_CIs[j];
+
+ ci.hContact = m_Sources[j];
+ ci.hEvent = db_event_first(ci.hContact);
+ ci.ei.dbe.pBlob = NULL;
+ ci.ei.nAllocated = 0;
+
+ fetchSlot(j);
+ }
+
+ fillQueue();
+}
+
+void MirandaContact::endRead()
+{
+#if defined(_DEBUG)
+ if (m_CIs.size() + m_EIs.size() + m_SpareEIs.size() > 0)
+ {
+ ext::string strLog = ext::str(ext::format(muT("Freeing | CIs and |+| EIs...\n")) % m_CIs.size() % m_EIs.size() % m_SpareEIs.size());
+
+ OutputDebugString(strLog.c_str());
+ }
+#endif // _DEBUG
+
+ citer_each_(std::vector<ContactInfo>, i, m_CIs)
+ {
+ free(i->ei.dbe.pBlob);
+ }
+
+ citer_each_(std::list<EventInfo>, i, m_EIs)
+ {
+ free(i->dbe.pBlob);
+ }
+
+ citer_each_(std::list<EventInfo>, i, m_SpareEIs)
+ {
+ free(i->dbe.pBlob);
+ }
+
+ m_CIs.clear();
+ m_EIs.clear();
+ m_SpareEIs.clear();
+}
+
+void MirandaContact::readNext()
+{
+ if (!m_EIs.empty())
+ {
+ m_SpareEIs.push_back(m_EIs.front());
+ m_EIs.pop_front();
+ }
+
+ fillQueue();
+}
+
+/*
+ * MirandaContactTolerantMerge
+ */
+
+void MirandaContactTolerantMerge::fillQueue()
+{
+ // assume that items with +/- 30 seconds may be equal
+ static const int timestampTol = 30;
+
+ while (!m_CIs.empty() && (m_EIs.size() < 2 || (m_EIs.back().dbe.timestamp - m_EIs.front().dbe.timestamp) <= timestampTol))
+ {
+ // find oldest next event in chains
+ int nNext = 0;
+ DWORD timestampFirst = m_CIs.front().ei.dbe.timestamp;
+
+ for (int i = 1; i < m_CIs.size(); ++i)
+ {
+ if (m_CIs[i].ei.dbe.timestamp < timestampFirst)
+ {
+ timestampFirst = m_CIs[i].ei.dbe.timestamp;
+ nNext = i;
+ }
+ }
+
+ // insert the fetched at correct position or throw away if duplicate
+ ContactInfo& ci = m_CIs[nNext];
+
+ std::list<EventInfo>::iterator insPos = m_EIs.end();
+ bool bIsDuplicate = false;
+
+ iter_each_(std::list<EventInfo>, j, m_EIs)
+ {
+ EventInfo& j_ei = *j;
+ int timestampDelta = j_ei.dbe.timestamp - ci.ei.dbe.timestamp;
+
+ if (timestampDelta > 0)
+ {
+ insPos = j;
+ }
+
+ if (j_ei.hContact != ci.ei.hContact &&
+ timestampDelta >= -timestampTol && timestampDelta <= timestampTol &&
+ j_ei.dbe.eventType == ci.ei.dbe.eventType &&
+ (j_ei.dbe.flags & ~(DBEF_FIRST | DBEF_READ)) == (ci.ei.dbe.flags & ~(DBEF_FIRST | DBEF_READ)) &&
+ j_ei.dbe.cbBlob == ci.ei.dbe.cbBlob &&
+ memcmp(j_ei.dbe.pBlob, ci.ei.dbe.pBlob, j_ei.dbe.cbBlob) == 0)
+ {
+ bIsDuplicate = true;
+
+ break;
+ }
+ }
+
+ if (!bIsDuplicate)
+ {
+ m_EIs.insert(insPos, ci.ei);
+
+ if (!m_SpareEIs.empty())
+ {
+ ci.ei = m_SpareEIs.front();
+ m_SpareEIs.pop_front();
+ }
+ else
+ {
+ ci.ei.dbe.pBlob = NULL;
+ ci.ei.nAllocated = 0;
+ }
+ }
+
+ fetchSlot(nNext);
+ }
+}
+
+/*
+ * MirandaContactStrictMerge
+ */
+
+void MirandaContactStrictMerge::fillQueue()
+{
+ // assume that items with +/- 30 seconds may be equal
+ static const int timestampTol = 0;
+
+ while (!m_CIs.empty() && (m_EIs.size() < 2 || (m_EIs.back().dbe.timestamp - m_EIs.front().dbe.timestamp) <= timestampTol))
+ {
+ // find oldest next event in chains
+ int nNext = 0;
+ DWORD timestampFirst = m_CIs.front().ei.dbe.timestamp;
+
+ for (int i = 1; i < m_CIs.size(); ++i)
+ {
+ if (m_CIs[i].ei.dbe.timestamp < timestampFirst)
+ {
+ timestampFirst = m_CIs[i].ei.dbe.timestamp;
+ nNext = i;
+ }
+ }
+
+ // insert the fetched at correct position or throw away if duplicate
+ ContactInfo& ci = m_CIs[nNext];
+
+ std::list<EventInfo>::iterator insPos = m_EIs.end();
+ bool bIsDuplicate = false;
+
+ iter_each_(std::list<EventInfo>, j, m_EIs)
+ {
+ EventInfo& j_ei = *j;
+ int timestampDelta = j_ei.dbe.timestamp - ci.ei.dbe.timestamp;
+
+ if (timestampDelta > 0)
+ {
+ insPos = j;
+ }
+
+ if (j_ei.hContact != ci.ei.hContact &&
+ timestampDelta >= -timestampTol && timestampDelta <= timestampTol &&
+ j_ei.dbe.eventType == ci.ei.dbe.eventType &&
+ (j_ei.dbe.flags & ~(DBEF_FIRST | DBEF_READ)) == (ci.ei.dbe.flags & ~(DBEF_FIRST | DBEF_READ)) &&
+ j_ei.dbe.cbBlob == ci.ei.dbe.cbBlob &&
+ memcmp(j_ei.dbe.pBlob, ci.ei.dbe.pBlob, j_ei.dbe.cbBlob) == 0)
+ {
+ bIsDuplicate = true;
+
+ break;
+ }
+ }
+
+ if (!bIsDuplicate)
+ {
+ m_EIs.insert(insPos, ci.ei);
+
+ if (!m_SpareEIs.empty())
+ {
+ ci.ei = m_SpareEIs.front();
+ m_SpareEIs.pop_front();
+ }
+ else
+ {
+ ci.ei.dbe.pBlob = NULL;
+ ci.ei.nAllocated = 0;
+ }
+ }
+
+ fetchSlot(nNext);
+ }
+}
+
+/*
+ * MirandaContactNoMerge
+ */
+
+void MirandaContactNoMerge::fillQueue()
+{
+ while (!m_CIs.empty() && m_EIs.size() < 1)
+ {
+ // find oldest next event in chains
+ int nNext = 0;
+ DWORD timestampFirst = m_CIs.front().ei.dbe.timestamp;
+
+ for (int i = 1; i < m_CIs.size(); ++i)
+ {
+ if (m_CIs[i].ei.dbe.timestamp < timestampFirst)
+ {
+ timestampFirst = m_CIs[i].ei.dbe.timestamp;
+ nNext = i;
+ }
+ }
+
+ // insert the fetched at correct position or throw away if duplicate
+ ContactInfo& ci = m_CIs[nNext];
+
+ m_EIs.push_back(ci.ei);
+
+ if (!m_SpareEIs.empty())
+ {
+ ci.ei = m_SpareEIs.front();
+ m_SpareEIs.pop_front();
+ }
+ else
+ {
+ ci.ei.dbe.pBlob = NULL;
+ ci.ei.nAllocated = 0;
+ }
+
+ fetchSlot(nNext);
+ }
+}
+
+/*
+ * MirandaContactFactory
+ */
+
+MirandaContact* MirandaContactFactory::makeMirandaContact(int MergeMode, const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const MirandaContact::SourceHandles& sources)
+{
+ switch (MergeMode)
+ {
+ case Settings::mmTolerantMerge:
+ return new MirandaContactTolerantMerge(strNick, strProtocol, strGroup, sources);
+
+ case Settings::mmStrictMerge:
+ return new MirandaContactStrictMerge(strNick, strProtocol, strGroup, sources);
+
+ case Settings::mmNoMerge:
+ return new MirandaContactNoMerge(strNick, strProtocol, strGroup, sources);
+
+ default:
+ return 0;
+ }
+}
diff --git a/plugins/HistoryStats/src/mirandacontact.h b/plugins/HistoryStats/src/mirandacontact.h
new file mode 100644
index 0000000000..c91f79853b
--- /dev/null
+++ b/plugins/HistoryStats/src/mirandacontact.h
@@ -0,0 +1,143 @@
+#if !defined(HISTORYSTATS_GUARD_MIRANDACONTACT_H)
+#define HISTORYSTATS_GUARD_MIRANDACONTACT_H
+
+#include "_globals.h"
+
+#include <vector>
+#include <list>
+
+/*
+ * MirandaContact
+ */
+
+class MirandaContact
+ : private pattern::NotCopyable<MirandaContact>
+{
+public:
+ struct EventInfo {
+ MCONTACT hContact;
+ DBEVENTINFO dbe;
+ int nAllocated;
+// int nDuplicates;
+ };
+
+ struct ContactInfo {
+ MCONTACT hContact;
+ HANDLE hEvent;
+ EventInfo ei;
+ };
+
+ typedef std::vector<MCONTACT> SourceHandles;
+
+private:
+ // general info
+ ext::string m_strNick;
+ ext::string m_strProtocol;
+ ext::string m_strGroup;
+ SourceHandles m_Sources;
+
+protected:
+ // reading messages
+ std::vector<ContactInfo> m_CIs;
+ std::list<EventInfo> m_EIs;
+ std::list<EventInfo> m_SpareEIs;
+
+private:
+ // reading messages
+ void stripMetaID(DBEVENTINFO& dbe);
+
+protected:
+ // reading messages
+ void fetchSlot(int i);
+ virtual void fillQueue() = 0;
+
+public:
+ explicit MirandaContact(const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const SourceHandles& sources);
+ virtual ~MirandaContact();
+
+ // general info
+ const ext::string& getNick() const { return m_strNick; }
+ const ext::string& getProtocol() const { return m_strProtocol; }
+ const ext::string& getGroup() const { return m_strGroup; }
+ const SourceHandles& getSources() const { return m_Sources; }
+
+ // merge
+ void merge(const MirandaContact& other);
+
+ // reading messages
+ void beginRead();
+ void endRead();
+ bool hasNext() { return !m_EIs.empty(); }
+ const DBEVENTINFO& getNext() { return m_EIs.front().dbe; }
+ void readNext();
+};
+
+/*
+ * MirandaContactTolerantMerge
+ */
+
+class MirandaContactTolerantMerge
+ : private pattern::NotCopyable<MirandaContactTolerantMerge>
+ , public MirandaContact
+{
+protected:
+ // reading message
+ virtual void fillQueue();
+
+public:
+ explicit MirandaContactTolerantMerge(const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const SourceHandles& sources)
+ : MirandaContact(strNick, strProtocol, strGroup, sources)
+ {
+ }
+};
+
+/*
+ * MirandaContactStrictMerge
+ */
+
+class MirandaContactStrictMerge
+ : private pattern::NotCopyable<MirandaContactStrictMerge>
+ , public MirandaContact
+{
+protected:
+ // reading message
+ virtual void fillQueue();
+
+public:
+ explicit MirandaContactStrictMerge(const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const SourceHandles& sources)
+ : MirandaContact(strNick, strProtocol, strGroup, sources)
+ {
+ }
+};
+
+/*
+ * MirandaContactNoMerge
+ */
+
+class MirandaContactNoMerge
+ : private pattern::NotCopyable<MirandaContactNoMerge>
+ , public MirandaContact
+{
+protected:
+ // reading message
+ virtual void fillQueue();
+
+public:
+ explicit MirandaContactNoMerge(const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const SourceHandles& sources)
+ : MirandaContact(strNick, strProtocol, strGroup, sources)
+ {
+ }
+};
+
+/*
+ * MirandaContactFactory
+ */
+
+class MirandaContactFactory
+ : public pattern::NotInstantiable<MirandaContactFactory>
+{
+public:
+ static MirandaContact* makeMirandaContact(int MergeMode, const ext::string& strNick, const ext::string& strProtocol, const ext::string& strGroup, const MirandaContact::SourceHandles& sources);
+};
+
+#endif // HISTORYSTATS_GUARD_MIRANDACONTACT_H
diff --git a/plugins/HistoryStats/src/mirandahistory.cpp b/plugins/HistoryStats/src/mirandahistory.cpp
new file mode 100644
index 0000000000..2e37729fe3
--- /dev/null
+++ b/plugins/HistoryStats/src/mirandahistory.cpp
@@ -0,0 +1,226 @@
+#include "_globals.h"
+#include "mirandahistory.h"
+
+#include "mirandasettings.h"
+
+/*
+ * MirandaHistory
+ */
+
+void MirandaHistory::populateProtocols()
+{
+ m_Protocols.clear();
+
+ PROTOACCOUNT **protoList;
+ int protoCount;
+
+ if (mu::proto::enumProtocols(&protoCount, &protoList) == 0)
+ {
+ upto_each_(i, protoCount)
+ {
+ ext::a::string protoName = protoList[i]->szModuleName;
+
+ Protocol& curProto = m_Protocols[protoName];
+
+ curProto.displayName = Protocol::getDisplayName(protoName);
+ }
+ }
+
+ m_DefaultProtocol.displayName = i18n(muT("(Unknown)"));
+}
+
+const Protocol& MirandaHistory::getProtocol(const ext::a::string& protocol) const
+{
+ std::map<ext::a::string, Protocol>::const_iterator i = m_Protocols.find(protocol);
+
+ return (i != m_Protocols.end()) ? i->second : m_DefaultProtocol;
+}
+
+void MirandaHistory::makeContactsAvailable()
+{
+ if (m_bContactsAvailable)
+ {
+ return;
+ }
+
+ // make protocols available
+ populateProtocols();
+
+ // first run:
+ // - enum all contacts
+ // - skip for ignored protocol
+ // - handle meta contacts (if enabled and available)
+ // - skip manually excluded
+ readContacts();
+
+ // second run:
+ // - merge contacts with similar names
+ mergeContacts();
+
+ m_bContactsAvailable = true;
+}
+
+void MirandaHistory::readContacts()
+{
+ bool bHandleMeta = mu::metacontacts::_available() && m_Settings.m_MetaContactsMode != Settings::mcmIgnoreMeta;
+ ext::a::string strMetaProto = bHandleMeta ? META_PROTO : muA("");
+ MirandaSettings db;
+
+ std::vector<MCONTACT> sources;
+
+ MCONTACT hContact = db_find_first();
+ while (hContact)
+ {
+ db.setContact(hContact);
+
+ const mu_ansi* pProtoName = GetContactProto(hContact);
+
+ // if something leads to ignorance of conact jump to end of
+ // processing this contact via 'break'
+ do
+ {
+ // ignore because of bad or not loaded protocol?
+ if (!pProtoName)
+ {
+ pProtoName = con::ProtoUnknown; // MEMO: alternative would be "break;"
+ }
+
+ ext::string curNick = mu::clist::getContactDisplayName(hContact);
+
+ // retrieve protocol
+ const ext::a::string curProtoName = pProtoName;
+ const Protocol& curProto = getProtocol(curProtoName);
+
+ // retrieve group
+ db.setModule(con::ModCList);
+ ext::string curGroup = db.readStrDirect(con::SettGroup, i18n(muT("(none)")));
+
+ // ignore because of filtered protocol?
+ if (m_Settings.m_ProtosIgnore.find(curProtoName) != m_Settings.m_ProtosIgnore.end())
+ break;
+
+ // init list of event sources
+ sources.clear();
+ sources.push_back(hContact);
+
+ // handle meta-contacts
+ if (bHandleMeta)
+ {
+ if (curProtoName == strMetaProto)
+ {
+ // don't include meta-contact history
+ if (m_Settings.m_MetaContactsMode == Settings::mcmSubOnly)
+ {
+ sources.clear();
+ }
+
+ // include meta-contact's subcontact
+ if (m_Settings.m_MetaContactsMode != Settings::mcmMetaOnly)
+ {
+ // find subcontacts to read history from
+ int numSubs = mu::metacontacts::getNumContacts(hContact);
+
+ if (numSubs > 0)
+ {
+ for (int i = 0; i < numSubs; ++i)
+ {
+ MCONTACT hSubContact = mu::metacontacts::getSubContact(hContact, i);
+
+ if (hSubContact)
+ {
+ sources.push_back(hSubContact);
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // ignore because of meta-contact?
+ if (db_mc_getMeta(hContact))
+ break;
+ }
+ }
+
+ // ignore because of exclude?
+ db.setModule(con::ModHistoryStats);
+
+ if (db.readBool(con::SettExclude, false))
+ {
+ break;
+ }
+
+ // finally add to list
+ MirandaContact* pContact = MirandaContactFactory::makeMirandaContact(m_Settings.m_MergeMode, curNick, curProto.displayName, curGroup, sources);
+
+ m_Contacts.push_back(pContact);
+ } while (false);
+
+ hContact = db_find_next(hContact);
+ }
+}
+
+void MirandaHistory::mergeContacts()
+{
+ if (!m_Settings.m_MergeContacts)
+ {
+ return;
+ }
+
+ for (ContactList::size_type i = 0; i < m_Contacts.size(); ++i)
+ {
+ MirandaContact& cur = *m_Contacts[i];
+
+ for (ContactList::size_type j = i + 1; j < m_Contacts.size(); ++j)
+ {
+ if (m_Contacts[j]->getNick() == cur.getNick())
+ {
+ if (!m_Settings.m_MergeContactsGroups || m_Contacts[j]->getGroup() == cur.getGroup())
+ {
+ cur.merge(*m_Contacts[j]);
+ delete m_Contacts[j];
+
+ m_Contacts.erase(m_Contacts.begin() + j);
+ --j;
+ }
+ }
+ }
+ }
+}
+
+MirandaHistory::MirandaHistory(const Settings& settings)
+ : m_Settings(settings), m_bContactsAvailable(false)
+{
+}
+
+MirandaHistory::~MirandaHistory()
+{
+ citer_each_(ContactList, i, m_Contacts)
+ {
+ delete *i;
+ }
+
+ m_Contacts.clear();
+}
+
+int MirandaHistory::getContactCount()
+{
+ if (!m_bContactsAvailable)
+ {
+ makeContactsAvailable();
+ }
+
+ return m_Contacts.size();
+}
+
+MirandaContact& MirandaHistory::getContact(int index)
+{
+ if (!m_bContactsAvailable)
+ {
+ makeContactsAvailable();
+ }
+
+ assert(index >= 0 && index < m_Contacts.size());
+
+ return *m_Contacts[index];
+}
diff --git a/plugins/HistoryStats/src/mirandahistory.h b/plugins/HistoryStats/src/mirandahistory.h
new file mode 100644
index 0000000000..6d344de891
--- /dev/null
+++ b/plugins/HistoryStats/src/mirandahistory.h
@@ -0,0 +1,41 @@
+#if !defined(HISTORYSTATS_GUARD_MIRANDAHISTORY_H)
+#define HISTORYSTATS_GUARD_MIRANDAHISTORY_H
+
+#include "_globals.h"
+
+#include <vector>
+
+#include "mirandacontact.h"
+#include "protocol.h"
+#include "settings.h"
+
+class MirandaHistory
+ : private pattern::NotCopyable<MirandaHistory>
+{
+private:
+ typedef std::map<ext::a::string, Protocol> ProtocolMap;
+ typedef std::vector<MirandaContact*> ContactList;
+
+private:
+ const Settings& m_Settings;
+ ProtocolMap m_Protocols;
+ Protocol m_DefaultProtocol;
+ bool m_bContactsAvailable;
+ ContactList m_Contacts;
+
+private:
+ void populateProtocols();
+ const Protocol& getProtocol(const ext::a::string& protocol) const;
+ void makeContactsAvailable();
+ void readContacts();
+ void mergeContacts();
+
+public:
+ explicit MirandaHistory(const Settings& settings);
+ ~MirandaHistory();
+
+ int getContactCount();
+ MirandaContact& getContact(int index);
+};
+
+#endif // HISTORYSTATS_GUARD_MIRANDAHISTORY_H
diff --git a/plugins/HistoryStats/src/mirandasettings.cpp b/plugins/HistoryStats/src/mirandasettings.cpp
new file mode 100644
index 0000000000..34d8342d38
--- /dev/null
+++ b/plugins/HistoryStats/src/mirandasettings.cpp
@@ -0,0 +1,143 @@
+#include "_globals.h"
+#include "mirandasettings.h"
+
+
+MirandaSettings::MirandaSettings()
+ : m_hContact(0)
+{
+}
+
+bool MirandaSettings::readBool(const mu_ansi* szSetting, bool bDefault) const
+{
+ return (readByte(szSetting, bDefault ? 1 : 0) != 0);
+}
+
+int MirandaSettings::readByte(const mu_ansi* szSetting, int bDefault) const
+{
+ return db_get_b(m_hContact, m_strModule.c_str(), szSetting, bDefault);
+}
+
+int MirandaSettings::readWord(const mu_ansi* szSetting, int wDefault) const
+{
+ return db_get_w(m_hContact, m_strModule.c_str(), szSetting, wDefault);
+}
+
+int MirandaSettings::readDWord(const mu_ansi* szSetting, int dwDefault) const
+{
+ return db_get_dw(m_hContact, m_strModule.c_str(), szSetting, dwDefault);
+}
+
+ext::string MirandaSettings::readStr(const mu_ansi* szSetting, const mu_text* szDefault) const
+{
+ DBVARIANT dbv;
+ if (db_get_s(m_hContact, m_strModule.c_str(), szSetting, &dbv))
+ return szDefault;
+
+ ext::string str = (dbv.type != DBVT_ASCIIZ) ? szDefault : utils::fromUTF8(dbv.pszVal);
+ db_free(&dbv);
+ return str;
+}
+
+ext::string MirandaSettings::readStrDirect(const mu_ansi* szSetting, const mu_text* szDefault) const
+{
+ DBVARIANT dbv;
+
+ ZeroMemory(&dbv, sizeof(dbv));
+
+ dbv.type = DBVT_WCHAR;
+
+ if (db_get_s(m_hContact, m_strModule.c_str(), szSetting, &dbv, 0))
+ return szDefault;
+
+ ext::string str;
+
+ switch (dbv.type) {
+ case DBVT_ASCIIZ:
+ str = utils::fromA(dbv.pszVal);
+ break;
+
+ case DBVT_WCHAR:
+ str = utils::fromW(dbv.pwszVal);
+ break;
+
+ case DBVT_UTF8:
+ str = utils::fromUTF8(dbv.pszVal);
+ break;
+
+ default:
+ str = szDefault;
+ break;
+ }
+
+ db_free(&dbv);
+ return str;
+}
+
+void MirandaSettings::readTree(const mu_ansi* szSetting, const mu_text* szDefault, SettingsTree& value) const
+{
+ value.fromString(readStr(szSetting, szDefault));
+}
+
+void MirandaSettings::writeBool(const mu_ansi* szSetting, bool bValue) const
+{
+ writeByte(szSetting, bValue ? 1 : 0);
+}
+
+void MirandaSettings::writeByte(const mu_ansi* szSetting, int bValue) const
+{
+ db_set_b(m_hContact, m_strModule.c_str(), szSetting, bValue);
+}
+
+void MirandaSettings::writeWord(const mu_ansi* szSetting, int wValue) const
+{
+ db_set_w(m_hContact, m_strModule.c_str(), szSetting, wValue);
+}
+
+void MirandaSettings::writeDWord(const mu_ansi* szSetting, int dwValue) const
+{
+ db_set_dw(m_hContact, m_strModule.c_str(), szSetting, dwValue);
+}
+
+void MirandaSettings::writeStr(const mu_ansi* szSetting, const mu_text* szValue) const
+{
+ db_set_ts(m_hContact, m_strModule.c_str(), szSetting, szValue);
+}
+
+void MirandaSettings::writeStrDirect(const mu_ansi* szSetting, const mu_text* szValue) const
+{
+ db_set_ts(m_hContact, m_strModule.c_str(), szSetting, szValue);
+}
+
+void MirandaSettings::writeTree(const mu_ansi* szSetting, const SettingsTree& value) const
+{
+ writeStr(szSetting, value.toString().c_str());
+}
+
+bool MirandaSettings::settingExists(const mu_ansi* szSetting) const
+{
+ DBVARIANT dbv;
+ if (db_get_s(m_hContact, m_strModule.c_str(), szSetting, &dbv, 0))
+ return false;
+
+ db_free(&dbv);
+ return true;
+}
+
+bool MirandaSettings::delSetting(const mu_ansi* szSetting)
+{
+ return db_unset(m_hContact, m_strModule.c_str(), szSetting) == 0;
+}
+
+int MirandaSettings::enumSettingsProc(const mu_ansi* szSetting, LPARAM lParam)
+{
+ SetInserter* pInserter = reinterpret_cast<SetInserter*>(lParam);
+
+ *pInserter = szSetting;
+
+ return 0;
+}
+
+void MirandaSettings::enumSettings(SetInserter& insertIterator)
+{
+ mu::db_contact::enumSettings(m_hContact, m_strModule.c_str(), enumSettingsProc, reinterpret_cast<LPARAM>(&insertIterator));
+}
diff --git a/plugins/HistoryStats/src/mirandasettings.h b/plugins/HistoryStats/src/mirandasettings.h
new file mode 100644
index 0000000000..827c9f4ac5
--- /dev/null
+++ b/plugins/HistoryStats/src/mirandasettings.h
@@ -0,0 +1,63 @@
+#if !defined(HISTORYSTATS_GUARD_MIRANDASETTINGS_H)
+#define HISTORYSTATS_GUARD_MIRANDASETTINGS_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include "utils.h"
+#include "settingstree.h"
+
+#include <set>
+#include <iterator>
+
+class MirandaSettings
+ : private pattern::NotCopyable<MirandaSettings>
+{
+public:
+ typedef std::set<ext::a::string> SettingsSet;
+ typedef std::insert_iterator<SettingsSet> SetInserter;
+
+private:
+ MCONTACT m_hContact;
+ ext::a::string m_strModule;
+
+private:
+ static int enumSettingsProc(const mu_ansi* szSetting, LPARAM lParam);
+
+public:
+ // constructor
+ explicit MirandaSettings();
+
+ // contact/module management
+ MCONTACT getContact() const { return m_hContact; }
+ void setContact(MCONTACT hContact) { m_hContact = hContact; }
+ const ext::a::string& getModule() const { return m_strModule; }
+ void setModule(const mu_ansi* module) { m_strModule = module; }
+
+ // reading
+ bool readBool(const mu_ansi* szSetting, bool bDefault) const;
+ int readByte(const mu_ansi* szSetting, int bDefault) const;
+ int readWord(const mu_ansi* szSetting, int wDefault) const;
+ int readDWord(const mu_ansi* szSetting, int dwDeault) const;
+ ext::string readStr(const mu_ansi* szSetting, const mu_text* szDefault) const;
+ ext::string readStrDirect(const mu_ansi* szSetting, const mu_text* szDefault) const;
+ void readTree(const mu_ansi* szSetting, const mu_text* szDefault, SettingsTree& value) const;
+
+ // writing
+ void writeBool(const mu_ansi* szSetting, bool bValue) const;
+ void writeByte(const mu_ansi* szSetting, int bValue) const;
+ void writeWord(const mu_ansi* szSetting, int wValue) const;
+ void writeDWord(const mu_ansi* szSetting, int dwValue) const;
+ void writeStr(const mu_ansi* szSetting, const mu_text* szValue) const;
+ void writeStrDirect(const mu_ansi* szSetting, const mu_text* szValue) const;
+ void writeTree(const mu_ansi* szSetting, const SettingsTree& value) const;
+
+ // misc functions
+ bool settingExists(const mu_ansi* szSetting) const;
+ bool delSetting(const mu_ansi* szSetting);
+
+ // enumeration
+ void enumSettings(SetInserter& insertIterator);
+};
+
+#endif // HISTORYSTATS_GUARD_MIRANDASETTINGS_H
diff --git a/plugins/HistoryStats/src/mu_common.cpp b/plugins/HistoryStats/src/mu_common.cpp
new file mode 100644
index 0000000000..4ab9da142a
--- /dev/null
+++ b/plugins/HistoryStats/src/mu_common.cpp
@@ -0,0 +1,527 @@
+#include "_globals.h"
+#include "mu_common.h"
+
+#include <map>
+#include <set>
+
+namespace mu
+{
+ /*
+ * clist
+ */
+
+ namespace clist
+ {
+ HANDLE addMainMenuItem(const mu_text* pszName, DWORD flags, int position, HICON hIcon, const mu_ansi* pszService, const mu_text* pszPopupName /* = NULL */, int popupPosition /* = 0 */, DWORD hotKey /* = 0 */)
+ {
+ // TODO: support for unicode-core with unicode-aware CList
+ CLISTMENUITEM mi;
+
+ ZeroMemory(&mi, sizeof(mi));
+
+ mi.cbSize = sizeof(mi);
+ mi.pszName = MU_DO_BOTH(const_cast<mu_ansi*>(pszName), wideToAnsiDup(pszName));
+ mi.flags = flags;
+ mi.position = position;
+ mi.hIcon = hIcon;
+ mi.pszService = const_cast<mu_ansi*>(pszService);
+ mi.pszPopupName = MU_DO_BOTH(const_cast<mu_ansi*>(pszPopupName), wideToAnsiDup(pszPopupName));
+ mi.popupPosition = popupPosition;
+ mi.hotKey = hotKey;
+
+ HANDLE res = Menu_AddMainMenuItem(&mi);
+
+ MU_DO_WIDE(freeAnsi(mi.pszName));
+ MU_DO_WIDE(freeAnsi(mi.pszPopupName));
+
+ return res;
+ }
+
+ HANDLE addContactMenuItem(const mu_text* pszName, DWORD flags, int position, HICON hIcon, const mu_ansi* pszService, DWORD hotKey /* = 0 */, const mu_ansi* pszContactOwner /* = NULL */)
+ {
+ // TODO: support for unicode-core with unicode-aware CList
+ CLISTMENUITEM mi;
+
+ ZeroMemory(&mi, sizeof(mi));
+
+ mi.cbSize = sizeof(mi);
+ mi.pszName = MU_DO_BOTH(const_cast<mu_ansi*>(pszName), wideToAnsiDup(pszName));
+ mi.flags = flags;
+ mi.position = position;
+ mi.hIcon = hIcon;
+ mi.pszService = const_cast<mu_ansi*>(pszService);
+ mi.hotKey = hotKey;
+ mi.pszContactOwner = const_cast<mu_ansi*>(pszContactOwner);
+
+ HANDLE res = Menu_AddContactMenuItem(&mi);
+
+ MU_DO_WIDE(freeAnsi(mi.pszName));
+
+ return res;
+ }
+
+ int modifyMenuItem(HANDLE hMenuItem, DWORD toModify, const mu_text* pszName /* = NULL */, DWORD flags /* = 0 */, HICON hIcon /* = NULL */, DWORD hotKey /* = 0 */)
+ {
+ // TODO: support for unicode-core with unicode-aware CList
+ CLISTMENUITEM mi;
+
+ ZeroMemory(&mi, sizeof(mi));
+
+ mi.cbSize = sizeof(mi);
+ mi.pszName = MU_DO_BOTH(const_cast<mu_ansi*>(pszName), wideToAnsiDup(pszName));
+ mi.flags = toModify | flags;
+ mi.hIcon = hIcon;
+ mi.hotKey = hotKey;
+
+ int res = CallService(MS_CLIST_MODIFYMENUITEM, reinterpret_cast<WPARAM>(hMenuItem), reinterpret_cast<LPARAM>(&mi));
+
+ MU_DO_WIDE(freeAnsi(mi.pszName));
+
+ return res;
+ }
+
+ const mu_text* getContactDisplayName(MCONTACT hContact)
+ {
+ return reinterpret_cast<const mu_text*>(CallService(MS_CLIST_GETCONTACTDISPLAYNAME, hContact, GCDNF_UNICODE));
+ }
+
+ const mu_text* getStatusModeDescription(int nStatusMode)
+ {
+ return reinterpret_cast<const mu_text*>(CallService(MS_CLIST_GETSTATUSMODEDESCRIPTION, static_cast<WPARAM>(nStatusMode), GSMDF_UNICODE));
+ }
+ }
+
+ /*
+ * db
+ */
+
+ namespace db
+ {
+ int getProfilePath(int cbName, mu_text* pszName)
+ {
+ // TODO: support for unicode core (if supported)
+#if defined(MU_WIDE)
+ mu_ansi* pszNameAnsi = new mu_ansi[cbName];
+
+ int ret = CallService(MS_DB_GETPROFILEPATH, cbName, reinterpret_cast<LPARAM>(pszNameAnsi));
+
+ if (ret == 0)
+ {
+ ansiToWide(pszNameAnsi, pszName, cbName);
+ }
+
+ delete pszNameAnsi;
+
+ return ret;
+#else // MU_WIDE
+ return CallService(MS_DB_GETPROFILEPATH, cbName, reinterpret_cast<LPARAM>(pszName));
+#endif // MU_WIDE
+ }
+
+ int getProfileName(int cbName, mu_text* pszName)
+ {
+ // TODO: support for unicode core (if supported)
+#if defined(MU_WIDE)
+ mu_ansi* pszNameAnsi = new mu_ansi[cbName];
+
+ int ret = CallService(MS_DB_GETPROFILENAME, cbName, reinterpret_cast<LPARAM>(pszNameAnsi));
+
+ if (ret == 0)
+ {
+ ansiToWide(pszNameAnsi, pszName, cbName);
+ }
+
+ delete pszNameAnsi;
+
+ return ret;
+#else // MU_WIDE
+ return CallService(MS_DB_GETPROFILENAME, cbName, reinterpret_cast<LPARAM>(pszName));
+#endif // MU_WIDE
+ }
+
+ void setSafetyMode(bool safetyMode)
+ {
+ CallService(MS_DB_SETSAFETYMODE, BOOL_(safetyMode), 0);
+ }
+ }
+
+ /*
+ * db_time
+ */
+
+ namespace db_contact
+ {
+ int enumSettings(MCONTACT hContact, const mu_ansi* szModule, DBSETTINGENUMPROC pEnumProc, LPARAM lProcParam)
+ {
+ DBCONTACTENUMSETTINGS dbces;
+
+ dbces.pfnEnumProc = pEnumProc;
+ dbces.lParam = lProcParam;
+ dbces.szModule = szModule;
+ dbces.ofsSettings = 0;
+
+ return CallService(MS_DB_CONTACT_ENUMSETTINGS, hContact, reinterpret_cast<LPARAM>(&dbces));
+ }
+ }
+
+ namespace db_time
+ {
+ DWORD timestampToLocal(DWORD timestamp)
+ {
+ return static_cast<DWORD>(CallService(MS_DB_TIME_TIMESTAMPTOLOCAL, static_cast<WPARAM>(timestamp), 0));
+ }
+ }
+
+ /*
+ * icolib
+ */
+
+ namespace icolib
+ {
+ bool _available()
+ {
+ return true;
+ }
+
+ void addIcon(const mu_text* szSection, const mu_text* szDescription, const mu_ansi* szIconName, const mu_ansi* szDefaultFile, int iDefaultIndex, int cx /* = 16 */, int cy /* = 16 */)
+ {
+ SKINICONDESC sid;
+
+ sid.cbSize = sizeof(sid);
+ sid.ptszSection = const_cast<mu_text*>(szSection);
+ sid.ptszDescription = const_cast<mu_text*>(szDescription);
+ sid.pszName = const_cast<mu_ansi*>(szIconName);
+ sid.pszDefaultFile = const_cast<mu_ansi*>(szDefaultFile);
+ sid.iDefaultIndex = iDefaultIndex;
+ sid.hDefaultIcon = NULL;
+ sid.cx = cx;
+ sid.cy = cy;
+ sid.flags = MU_DO_BOTH(0, SIDF_UNICODE);
+ Skin_AddIcon(&sid);
+ }
+
+ void addIcon(const mu_text* szSection, const mu_text* szDescription, const mu_ansi* szIconName, HICON hDefaultIcon, int cx /* = 16 */, int cy /* = 16 */)
+ {
+ SKINICONDESC sid;
+
+ sid.cbSize = sizeof(sid);
+ sid.ptszSection = const_cast<mu_text*>(szSection);
+ sid.ptszDescription = const_cast<mu_text*>(szDescription);
+ sid.pszName = const_cast<mu_ansi*>(szIconName);
+ sid.pszDefaultFile = NULL;
+ sid.iDefaultIndex = 0;
+ sid.hDefaultIcon = hDefaultIcon;
+ sid.cx = cx;
+ sid.cy = cy;
+ sid.flags = MU_DO_BOTH(0, SIDF_UNICODE);
+ Skin_AddIcon(&sid);
+ }
+
+ HICON getIcon(const mu_ansi* szIconName)
+ {
+ return reinterpret_cast<HICON>(CallService(MS_SKIN2_GETICON, 0, reinterpret_cast<LPARAM>(szIconName)));
+ }
+ }
+
+ /*
+ * langpack
+ */
+
+ namespace langpack
+ {
+ const mu_text* translateString(const mu_text* szEnglish)
+ {
+ return reinterpret_cast<const mu_text*>(CallService(MS_LANGPACK_TRANSLATESTRING, MU_DO_BOTH(0, LANG_UNICODE), reinterpret_cast<LPARAM>(szEnglish)));
+ }
+
+ UINT getCodePage()
+ {
+ static UINT CodePage = -1;
+
+ if (CodePage == -1)
+ {
+ CodePage = ServiceExists(MS_LANGPACK_GETCODEPAGE) ? CallService(MS_LANGPACK_GETCODEPAGE, 0, 0) : CP_ACP;
+ }
+
+ return CodePage;
+ }
+ }
+
+ /*
+ * metacontacts [external]
+ */
+
+ namespace metacontacts
+ {
+ bool _available()
+ {
+ return true;
+ }
+
+ int getNumContacts(MCONTACT hMetaContact)
+ {
+ return CallService(MS_MC_GETNUMCONTACTS, hMetaContact, 0);
+ }
+
+ MCONTACT getSubContact(MCONTACT hMetaContact, int iContactNumber)
+ {
+ return CallService(MS_MC_GETSUBCONTACT, hMetaContact, iContactNumber);
+ }
+ }
+
+ /*
+ * opt
+ */
+
+ namespace opt
+ {
+ void addPage(WPARAM addInfo, const mu_text* pszGroup, const mu_text* pszTitle, const mu_text* pszTab, DLGPROC pfnDlgProc, const mu_ansi* pszTemplate, HINSTANCE hInstance, DWORD flags /* = ODPF_BOLDGROUPS */)
+ {
+ OPTIONSDIALOGPAGE odp = { sizeof(odp) };
+ odp.ptszTitle = const_cast<mu_text*>(pszTitle);
+ odp.pfnDlgProc = pfnDlgProc;
+ odp.pszTemplate = const_cast<mu_ansi*>(pszTemplate);
+ odp.hInstance = hInstance;
+ odp.ptszGroup = const_cast<mu_text*>(pszGroup);
+ odp.flags = flags | MU_DO_BOTH(0, ODPF_UNICODE);
+ odp.ptszTab = const_cast<mu_text*>(pszTab);
+ Options_AddPage(addInfo, &odp);
+ }
+ }
+
+ /*
+ * png
+ */
+
+ namespace png
+ {
+ bool _available()
+ {
+ return
+ true &&
+ ServiceExists(MS_DIB2PNG);
+ }
+
+ bool dibToPng(const BITMAPINFOHEADER* pBMIH, const BYTE* pDIData, BYTE* pImageData, long* pImageLen)
+ {
+ DIB2PNG info;
+
+ info.pbmi = const_cast<BITMAPINFO*>(reinterpret_cast<const BITMAPINFO*>(pBMIH));
+ info.pDiData = const_cast<BYTE*>(pDIData);
+ info.pResult = pImageData;
+ info.pResultLen = pImageLen;
+
+ return bool_(CallService(MS_DIB2PNG, 0, reinterpret_cast<LPARAM>(&info)));
+ }
+ }
+
+ /*
+ * proto
+ */
+
+ namespace proto
+ {
+ int enumProtocols(int* numProtocols, PROTOACCOUNT*** ppProtoDescriptors)
+ {
+ return ProtoEnumAccounts(numProtocols, ppProtoDescriptors);
+ }
+
+ const mu_ansi* getContactBaseProto(MCONTACT hContact)
+ {
+ return reinterpret_cast<const mu_ansi*>(CallService(MS_PROTO_GETCONTACTBASEPROTO, hContact, 0));
+ }
+ }
+
+ /*
+ * protosvc
+ */
+
+ namespace protosvc
+ {
+ DWORD getCaps(const mu_ansi* szProto, int flagNum)
+ {
+ return (DWORD)CallProtoService(szProto, PS_GETCAPS, static_cast<WPARAM>(flagNum), 0);
+ }
+
+ int getName(const mu_ansi* szProto, int cchName, mu_text* szName)
+ {
+ return CallProtoService(szProto, PS_GETNAME, static_cast<WPARAM>(cchName), reinterpret_cast<LPARAM>(szName));
+ }
+
+ HICON loadIcon(const mu_ansi* szProto, int whichIcon)
+ {
+ return reinterpret_cast<HICON>(CallProtoService(szProto, PS_LOADICON, static_cast<WPARAM>(whichIcon), 0));
+ }
+ }
+
+ /*
+ * skin
+ */
+
+ namespace skin
+ {
+ HICON loadIcon(int id)
+ {
+ return reinterpret_cast<HICON>(CallService(MS_SKIN_LOADICON, id, 0));
+ }
+ }
+
+ /*
+ * system
+ */
+
+ namespace system
+ {
+ DWORD getVersion()
+ {
+ return static_cast<DWORD>(CallService(MS_SYSTEM_GETVERSION, 0, 0));
+ }
+
+ int getVersionText(int cchVersion, mu_ansi* szVersion)
+ {
+ return CallService(MS_SYSTEM_GETVERSIONTEXT, cchVersion, reinterpret_cast<LPARAM>(szVersion));
+ }
+
+ int terminated()
+ {
+ return CallService(MS_SYSTEM_TERMINATED, 0, 0);
+ }
+ }
+
+ /*
+ * utils
+ */
+
+ namespace utils
+ {
+ int pathToRelative(const mu_text* pszPath, mu_text* pszNewPath)
+ {
+ return CallService(MU_DO_BOTH(MS_UTILS_PATHTORELATIVE, MS_UTILS_PATHTORELATIVEW), reinterpret_cast<WPARAM>(pszPath), reinterpret_cast<LPARAM>(pszNewPath));
+ }
+
+ int pathToAbsolute(const mu_text* pszPath, mu_text* pszNewPath)
+ {
+ return CallService(MU_DO_BOTH(MS_UTILS_PATHTOABSOLUTE, MS_UTILS_PATHTOABSOLUTEW), reinterpret_cast<WPARAM>(pszPath), reinterpret_cast<LPARAM>(pszNewPath));
+ }
+ }
+
+ /*
+ * core interface functions
+ */
+
+ bool load()
+ {
+ // check for version
+ if (!isMirandaVersionOk(system::getVersion()))
+ return false;
+
+ return true;
+ }
+
+ void unload()
+ {
+ }
+
+ DWORD getMinimalMirandaVersion()
+ {
+ // MEMO: version dependency check
+ return PLUGIN_MAKE_VERSION(0, 6, 7, 0);
+ }
+
+ bool isMirandaVersionOk(DWORD version)
+ {
+ return (version >= getMinimalMirandaVersion());
+ }
+
+ bool isMirandaUnicode()
+ {
+ if (system::getVersion() < PLUGIN_MAKE_VERSION(0, 4, 3, 33))
+ {
+ return false;
+ }
+
+ mu_ansi szVersion[256] = { 0 };
+
+ if (system::getVersionText(256, szVersion) != 0)
+ {
+ return false;
+ }
+
+ if (!strstr(szVersion, muA("Unicode")))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /*
+ * string handling
+ */
+
+ mu_ansi* wideToAnsiDup(const mu_wide* pszWide, UINT uCP /* = CP_ACP */)
+ {
+ if (!pszWide)
+ {
+ return NULL;
+ }
+
+ int len = WideCharToMultiByte(uCP, 0, pszWide, -1, NULL, 0, NULL, NULL);
+ mu_ansi* result = reinterpret_cast<mu_ansi*>(malloc(sizeof(mu_ansi) * len));
+
+ if (!result)
+ {
+ return NULL;
+ }
+
+ WideCharToMultiByte(uCP, 0, pszWide, -1, result, len, NULL, NULL);
+ result[len - 1] = 0;
+
+ return result;
+ }
+
+ mu_wide* ansiToWideDup(const mu_ansi* pszAnsi, UINT uCP /* = CP_ACP */)
+ {
+ if (!pszAnsi)
+ {
+ return NULL;
+ }
+
+ int len = MultiByteToWideChar(uCP, 0, pszAnsi, -1, NULL, 0);
+ mu_wide* result = reinterpret_cast<mu_wide*>(malloc(sizeof(mu_wide) * len));
+
+ if (!result)
+ {
+ return NULL;
+ }
+
+ MultiByteToWideChar(uCP, 0, pszAnsi, -1, result, len);
+ result[len - 1] = 0;
+
+ return result;
+ }
+
+ mu_ansi* wideToAnsi(const mu_wide* pszWide, mu_ansi* pszRes, int maxLen, UINT uCP /* = CP_ACP */)
+ {
+ if (!pszWide)
+ {
+ return NULL;
+ }
+
+ WideCharToMultiByte(uCP, 0, pszWide, -1, pszRes, maxLen, NULL, NULL);
+
+ return pszRes;
+ }
+
+ mu_wide* ansiToWide(const mu_ansi* pszAnsi, mu_wide* pszRes, int maxLen, UINT uCP /* = CP_ACP */)
+ {
+ if (!pszAnsi)
+ {
+ return NULL;
+ }
+
+ MultiByteToWideChar(uCP, 0, pszAnsi, -1, pszRes, maxLen);
+
+ return pszRes;
+ }
+}
diff --git a/plugins/HistoryStats/src/mu_common.h b/plugins/HistoryStats/src/mu_common.h
new file mode 100644
index 0000000000..5c103743fd
--- /dev/null
+++ b/plugins/HistoryStats/src/mu_common.h
@@ -0,0 +1,291 @@
+#if !defined(HISTORYSTATS_GUARD_MU_COMMON_H)
+#define HISTORYSTATS_GUARD_MU_COMMON_H
+
+/*
+ * mu = miranda unified services
+ */
+
+#define _WIN32_WINDOWS 0x0500 // for WM_MOUSEWHEEL
+#define _WIN32_WINNT 0x0501 // for WM_THEMECHANGED
+
+#include <windows.h>
+#include <tchar.h>
+#include <stdio.h>
+
+/*
+ * include miranda headers
+ */
+
+#define MIRANDA_VER 0x0A00
+
+#include <newpluginapi.h>
+
+#include <m_awaymsg.h> // not used
+#include <m_button.h> // not used
+#include <m_chat.h> // not used
+#include <m_clc.h>
+#include <m_clist.h>
+#include <m_clistint.h> // not used
+#include <m_clui.h> // not used
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_email.h> // not used
+#include <m_file.h> // not used
+#include <m_findadd.h> // not used
+#include <m_fontservice.h> // not used
+#include <m_genmenu.h> // not used
+#include <m_history.h> // not used
+#include <m_icolib.h>
+#include <m_idle.h> // not used
+#include <m_ignore.h> // not used
+#include <m_langpack.h>
+#include <m_message.h> // not used
+#include <m_netlib.h> // not used
+#include <m_options.h>
+#include <m_png.h>
+#include <m_popup.h> // not used
+#include <m_protocols.h>
+#include <m_protomod.h> // not used
+#include <m_protosvc.h>
+#include <m_skin.h>
+#include <m_system.h>
+#include <m_system_cpp.h> // not used
+#include <m_url.h> // not used
+#include <m_userinfo.h> // not used
+#include <m_utils.h>
+
+#include <m_addcontact.h> // not used, depends on m_protosvc.h
+#include <m_icq.h> // depends on m_protosvc.h
+
+#include <m_metacontacts.h>
+#include <m_historystats.h> // our own header
+
+/*
+ * basic defines
+ */
+
+#if defined(_UNICODE)
+ #undef MU_ANSI
+ #define MU_WIDE
+#else
+ #define MU_ANSI
+ #undef MU_WIDE
+#endif
+
+/*
+ * helper macros to avoid many "#if defined(MU_WIDE) ... #else ... #endif" constructs
+ */
+
+#if defined(MU_WIDE)
+ #define MU_DO_BOTH(ansi, wide) wide
+ #define MU_DO_WIDE(wide) wide
+ #define MU_DO_ANSI(ansi) (void) 0
+#else
+ #define MU_DO_BOTH(ansi, wide) ansi
+ #define MU_DO_WIDE(wide) (void) 0
+ #define MU_DO_ANSI(ansi) ansi
+#endif
+
+/*
+ * common types mu_wide/mu_ansi/mu_text
+ */
+
+typedef wchar_t mu_wide;
+typedef char mu_ansi;
+typedef MU_DO_BOTH(char, wchar_t) mu_text;
+
+/*
+ * common macros for wrapping text
+ */
+
+#define muC(x) x
+#define muW(x) L##x
+#define muA(x) x
+#define muT(x) MU_DO_BOTH(muA(x), muW(x))
+
+/*
+ * helper functions
+ */
+
+namespace mu
+{
+ /*
+ * clist
+ */
+
+ namespace clist
+ {
+ HANDLE addMainMenuItem(const mu_text* pszName, DWORD flags, int position, HICON hIcon, const mu_ansi* pszService, const mu_text* pszPopupName = NULL, int popupPosition = 0, DWORD hotKey = 0);
+ HANDLE addContactMenuItem(const mu_text* pszName, DWORD flags, int position, HICON hIcon, const mu_ansi* pszService, DWORD hotKey = 0, const mu_ansi* pszContactOwner = NULL);
+ int modifyMenuItem(HANDLE hMenuItem, DWORD toModify, const mu_text* pszName = NULL, DWORD flags = 0, HICON hIcon = NULL, DWORD hotKey = 0);
+ const mu_text* getContactDisplayName(MCONTACT hContact);
+ const mu_text* getStatusModeDescription(int nStatusMode);
+ }
+
+ /*
+ * db
+ */
+
+ namespace db
+ {
+ int getProfilePath(int cbName, mu_text* pszName);
+ int getProfileName(int cbName, mu_text* pszName);
+ void setSafetyMode(bool safetyMode);
+ }
+
+ /*
+ * db_contact
+ */
+
+ namespace db_contact
+ {
+ int enumSettings(MCONTACT hContact, const mu_ansi* szModule, DBSETTINGENUMPROC pEnumProc, LPARAM lProcParam);
+ int getCount();
+ }
+
+ /*
+ * db_time
+ */
+
+ namespace db_time
+ {
+ DWORD timestampToLocal(DWORD timestamp);
+ }
+
+ /*
+ * icolib
+ */
+
+ namespace icolib
+ {
+ bool _available();
+ void addIcon(const mu_text* szSection, const mu_text* szDescription, const mu_ansi* szIconName, const mu_ansi* szDefaultFile, int iDefaultIndex, int cx = 16, int cy = 16);
+ void addIcon(const mu_text* szSection, const mu_text* szDescription, const mu_ansi* szIconName, HICON hDefaultIcon, int cx = 16, int cy = 16);
+ HICON getIcon(const mu_ansi* szIconName);
+ }
+
+ /*
+ * langpack
+ */
+
+ namespace langpack
+ {
+ int translateDialog(HWND hwndDlg, DWORD flags = 0, const int* ignoreControls = NULL);
+ const mu_text* translateString(const mu_text* szEnglish);
+ UINT getCodePage();
+ }
+
+ /*
+ * metacontacts [external]
+ */
+
+ namespace metacontacts
+ {
+ bool _available();
+ int getNumContacts(MCONTACT hMetaContact);
+ MCONTACT getSubContact(MCONTACT hMetaContact, int iContactNumber);
+ }
+
+ /*
+ * opt
+ */
+
+ namespace opt
+ {
+ void addPage(WPARAM addInfo, const mu_text* pszGroup, const mu_text* pszTitle, const mu_text* pszTab, DLGPROC pfnDlgProc, const mu_ansi* pszTemplate, HINSTANCE hInstance, DWORD flags = ODPF_BOLDGROUPS);
+ }
+
+ /*
+ * png
+ */
+
+ namespace png
+ {
+ bool _available();
+ bool dibToPng(const BITMAPINFOHEADER* pBMIH, const BYTE* pDIData, BYTE* pImageData, long* pImageLen);
+ }
+
+ /*
+ * proto
+ */
+
+ namespace proto
+ {
+ int enumProtocols(int* numProtocols, PROTOACCOUNT*** ppProtoDescriptors);
+ const mu_ansi* getContactBaseProto(MCONTACT hContact);
+ }
+
+ /*
+ * protosvc
+ */
+
+ namespace protosvc
+ {
+ DWORD getCaps(const mu_ansi* szProto, int flagNum);
+ int getName(const mu_ansi* szProto, int cchName, mu_text* szName);
+ HICON loadIcon(const mu_ansi* szProto, int whichIcon);
+ }
+
+ /*
+ * skin
+ */
+
+ namespace skin
+ {
+ HICON loadIcon(int id);
+ }
+
+ /*
+ * system
+ */
+
+ namespace system
+ {
+ DWORD getVersion();
+ int getVersionText(int cchVersion, mu_ansi* szVersion);
+ int terminated();
+ }
+
+ /*
+ * updater [external]
+ */
+
+ namespace updater
+ {
+ bool _available();
+ void registerFL(int fileID, const PLUGININFOEX* pluginInfo);
+ }
+
+ /*
+ * utils
+ */
+
+ namespace utils
+ {
+ int pathToRelative(const mu_text* pszPath, mu_text* pszNewPath);
+ int pathToAbsolute(const mu_text* pszPath, mu_text* pszNewPath);
+ }
+
+ /*
+ * core interface functions
+ */
+
+ bool load();
+ void unload();
+ DWORD getMinimalMirandaVersion();
+ bool isMirandaVersionOk(DWORD version);
+ bool isMirandaUnicode();
+
+ /*
+ * string handling
+ */
+
+ mu_ansi* wideToAnsiDup(const mu_wide* pszWide, UINT uCP = CP_ACP);
+ mu_wide* ansiToWideDup(const mu_ansi* pszAnsi, UINT uCP = CP_ACP);
+ mu_ansi* wideToAnsi(const mu_wide* pszWide, mu_ansi* pszRes, int maxLen, UINT uCP = CP_ACP);
+ mu_wide* ansiToWide(const mu_ansi* pszAnsi, mu_wide* pszRes, int maxLen, UINT uCP = CP_ACP);
+ inline void freeWide(mu_wide* pszWide) { free(pszWide); }
+ inline void freeAnsi(mu_ansi* pszAnsi) { free(pszAnsi); }
+}
+
+#endif // HISTORYSTATS_GUARD_MU_COMMON_H
diff --git a/plugins/HistoryStats/src/optionsctrl.cpp b/plugins/HistoryStats/src/optionsctrl.cpp
new file mode 100644
index 0000000000..dd0d077f26
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrl.cpp
@@ -0,0 +1,306 @@
+#include "_globals.h"
+#include "optionsctrl.h"
+
+#include "utils.h"
+
+/*
+ * OptionsCtrl
+ */
+
+HANDLE OptionsCtrl::insertGroup(HANDLE hParent, const mu_text* szLabel, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCGROUP ocg;
+
+ ocg.dwFlags = dwFlags;
+ ocg.szLabel = const_cast<mu_text*>(szLabel);
+ ocg.dwData = dwData;
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTGROUP, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&ocg)));
+}
+
+HANDLE OptionsCtrl::insertCheck(HANDLE hParent, const mu_text* szLabel, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCCHECK occ;
+
+ occ.dwFlags = dwFlags;
+ occ.szLabel = const_cast<mu_text*>(szLabel);
+ occ.dwData = dwData;
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTCHECK, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&occ)));
+}
+
+HANDLE OptionsCtrl::insertRadio(HANDLE hParent, HANDLE hSibling, const mu_text* szLabel, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCRADIO ocr;
+
+ ocr.dwFlags = dwFlags;
+ ocr.szLabel = const_cast<mu_text*>(szLabel);
+ ocr.dwData = dwData;
+ ocr.hSibling = hSibling;
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTRADIO, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&ocr)));
+}
+
+HANDLE OptionsCtrl::insertEdit(HANDLE hParent, const mu_text* szLabel, const mu_text* szEdit /* = muT("") */, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCEDIT oce;
+
+ oce.dwFlags = dwFlags;
+ oce.szLabel = const_cast<mu_text*>(szLabel);
+ oce.dwData = dwData;
+ oce.szEdit = const_cast<mu_text*>(szEdit);
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTEDIT, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&oce)));
+}
+
+HANDLE OptionsCtrl::insertCombo(HANDLE hParent, const mu_text* szLabel, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCCOMBO occ;
+
+ occ.dwFlags = dwFlags;
+ occ.szLabel = const_cast<mu_text*>(szLabel);
+ occ.dwData = dwData;
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTCOMBO, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&occ)));
+}
+
+HANDLE OptionsCtrl::insertButton(HANDLE hParent, const mu_text* szLabel, const mu_text* szButton, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCBUTTON ocb;
+
+ ocb.dwFlags = dwFlags;
+ ocb.szLabel = const_cast<mu_text*>(szLabel);
+ ocb.dwData = dwData;
+ ocb.szButton = const_cast<mu_text*>(szButton);
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTBUTTON, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&ocb)));
+}
+
+HANDLE OptionsCtrl::insertDateTime(HANDLE hParent, const mu_text* szLabel, DWORD dwDateTime, const mu_text* szFormat /* = muT("%Y-%m-%d") */, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCDATETIME ocdt;
+
+ ocdt.dwFlags = dwFlags;
+ ocdt.szLabel = const_cast<mu_text*>(szLabel);
+ ocdt.dwData = dwData;
+ ocdt.szFormat = const_cast<mu_text*>(szFormat);
+ ocdt.dwDateTime = dwDateTime;
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTDATETIME, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&ocdt)));
+}
+
+HANDLE OptionsCtrl::insertColor(HANDLE hParent, const mu_text* szLabel, COLORREF crColor /* = 0 */, DWORD dwFlags /* = 0 */, DWORD dwData /* = 0 */)
+{
+ OCCOLOR occ;
+
+ occ.dwFlags = dwFlags;
+ occ.szLabel = const_cast<mu_text*>(szLabel);
+ occ.dwData = dwData;
+ occ.crColor = crColor;
+
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_INSERTCOLOR, reinterpret_cast<WPARAM>(hParent), reinterpret_cast<LPARAM>(&occ)));
+}
+
+const mu_text* OptionsCtrl::getItemLabel(HANDLE hItem)
+{
+ return reinterpret_cast<const mu_text*>(SendMessage(m_hOptWnd, OCM_GETITEMLABEL, reinterpret_cast<WPARAM>(hItem), 0));
+}
+
+void OptionsCtrl::setItemLabel(HANDLE hItem, const mu_text* szLabel)
+{
+ SendMessage(m_hOptWnd, OCM_SETITEMLABEL, reinterpret_cast<WPARAM>(hItem), reinterpret_cast<LPARAM>(szLabel));
+}
+
+bool OptionsCtrl::isItemEnabled(HANDLE hItem)
+{
+ return bool_(SendMessage(m_hOptWnd, OCM_ISITEMENABLED, reinterpret_cast<WPARAM>(hItem), 0));
+}
+
+void OptionsCtrl::enableItem(HANDLE hItem, bool bEnable)
+{
+ SendMessage(m_hOptWnd, OCM_ENABLEITEM, reinterpret_cast<WPARAM>(hItem), BOOL_(bEnable));
+}
+
+DWORD OptionsCtrl::getItemData(HANDLE hItem)
+{
+ return SendMessage(m_hOptWnd, OCM_GETITEMDATA, reinterpret_cast<WPARAM>(hItem), 0);
+}
+
+void OptionsCtrl::setItemData(HANDLE hItem, DWORD dwData)
+{
+ SendMessage(m_hOptWnd, OCM_SETITEMDATA, reinterpret_cast<WPARAM>(hItem), dwData);
+}
+
+bool OptionsCtrl::isItemChecked(HANDLE hItem)
+{
+ return bool_(SendMessage(m_hOptWnd, OCM_ISITEMCHECKED, reinterpret_cast<WPARAM>(hItem), 0));
+}
+
+void OptionsCtrl::checkItem(HANDLE hItem, bool bCheck)
+{
+ SendMessage(m_hOptWnd, OCM_CHECKITEM, reinterpret_cast<WPARAM>(hItem), BOOL_(bCheck));
+}
+
+int OptionsCtrl::getRadioChecked(HANDLE hRadio)
+{
+ return SendMessage(m_hOptWnd, OCM_GETRADIOCHECKED, reinterpret_cast<WPARAM>(hRadio), 0);
+}
+
+void OptionsCtrl::setRadioChecked(HANDLE hRadio, int nCheck)
+{
+ SendMessage(m_hOptWnd, OCM_SETRADIOCHECKED, reinterpret_cast<WPARAM>(hRadio), nCheck);
+}
+
+int OptionsCtrl::getEditNumber(HANDLE hEdit)
+{
+ return SendMessage(m_hOptWnd, OCM_GETEDITNUMBER, reinterpret_cast<WPARAM>(hEdit), 0);
+}
+
+void OptionsCtrl::setEditNumber(HANDLE hEdit, int nNumber)
+{
+ SendMessage(m_hOptWnd, OCM_SETEDITNUMBER, reinterpret_cast<WPARAM>(hEdit), nNumber);
+}
+
+const mu_text* OptionsCtrl::getEditString(HANDLE hEdit)
+{
+ return reinterpret_cast<const mu_text*>(SendMessage(m_hOptWnd, OCM_GETEDITSTRING, reinterpret_cast<WPARAM>(hEdit), 0));
+}
+
+void OptionsCtrl::setEditString(HANDLE hEdit, const mu_text* szString)
+{
+ SendMessage(m_hOptWnd, OCM_SETEDITSTRING, reinterpret_cast<WPARAM>(hEdit), reinterpret_cast<LPARAM>(szString));
+}
+
+void OptionsCtrl::addComboItem(HANDLE hCombo, const mu_text* szItem)
+{
+ SendMessage(m_hOptWnd, OCM_ADDCOMBOITEM, reinterpret_cast<WPARAM>(hCombo), reinterpret_cast<LPARAM>(szItem));
+}
+
+int OptionsCtrl::getComboSelected(HANDLE hCombo)
+{
+ return SendMessage(m_hOptWnd, OCM_GETCOMBOSELECTED, reinterpret_cast<WPARAM>(hCombo), 0);
+}
+
+void OptionsCtrl::setComboSelected(HANDLE hCombo, int nSelect)
+{
+ SendMessage(m_hOptWnd, OCM_SETCOMBOSELECTED, reinterpret_cast<WPARAM>(hCombo), nSelect);
+}
+
+void OptionsCtrl::ensureVisible(HANDLE hItem)
+{
+ SendMessage(m_hOptWnd, OCM_ENSUREVISIBLE, reinterpret_cast<WPARAM>(hItem), 0);
+}
+
+void OptionsCtrl::deleteAllItems()
+{
+ SendMessage(m_hOptWnd, OCM_DELETEALLITEMS, 0, 0);
+}
+
+HANDLE OptionsCtrl::getSelection()
+{
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_GETSELECTION, 0, 0));
+}
+
+void OptionsCtrl::selectItem(HANDLE hItem)
+{
+ SendMessage(m_hOptWnd, OCM_SELECTITEM, reinterpret_cast<WPARAM>(hItem), 0);
+}
+
+HANDLE OptionsCtrl::getFirstItem()
+{
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_GETITEM, 0, OCGI_FIRST));
+}
+
+HANDLE OptionsCtrl::getNextItem(HANDLE hItem)
+{
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_GETITEM, reinterpret_cast<WPARAM>(hItem), OCGI_NEXT));
+}
+
+HANDLE OptionsCtrl::getPrevItem(HANDLE hItem)
+{
+ return reinterpret_cast<HANDLE>(SendMessage(m_hOptWnd, OCM_GETITEM,reinterpret_cast<WPARAM>(hItem), OCGI_PREV));
+}
+
+void OptionsCtrl::setRedraw(bool bRedraw)
+{
+ SendMessage(m_hOptWnd, WM_SETREDRAW, BOOL_(bRedraw), 0);
+}
+
+void OptionsCtrl::deleteItem(HANDLE hItem)
+{
+ SendMessage(m_hOptWnd, OCM_DELETEITEM, reinterpret_cast<WPARAM>(hItem), 0);
+}
+
+void OptionsCtrl::moveItem(HANDLE& hItem, HANDLE hInsertAfter)
+{
+ SendMessage(m_hOptWnd, OCM_MOVEITEM, reinterpret_cast<WPARAM>(&hItem), reinterpret_cast<LPARAM>(hInsertAfter));
+}
+
+int OptionsCtrl::getScrollPos(int nBar)
+{
+ return SendMessage(m_hOptWnd, OCM_GETSCROLLPOS, nBar, 0);
+}
+
+void OptionsCtrl::setScrollPos(int nBar, int nPos)
+{
+ SendMessage(m_hOptWnd, OCM_SETSCROLLPOS, nBar, nPos);
+}
+
+bool OptionsCtrl::isDateTimeNone(HANDLE hDateTime)
+{
+ return bool_(SendMessage(m_hOptWnd, OCM_ISDATETIMENONE, reinterpret_cast<WPARAM>(hDateTime), 0));
+}
+
+void OptionsCtrl::setDateTimeNone(HANDLE hDateTime)
+{
+ SendMessage(m_hOptWnd, OCM_SETDATETIMENONE, reinterpret_cast<LPARAM>(hDateTime), 0);
+}
+
+DWORD OptionsCtrl::getDateTime(HANDLE hDateTime, bool* pbNone /* = NULL */)
+{
+ BOOL bMyNone = FALSE;
+ DWORD dwResult = SendMessage(m_hOptWnd, OCM_GETDATETIME, reinterpret_cast<WPARAM>(hDateTime), reinterpret_cast<LPARAM>(&bMyNone));
+
+ if (pbNone)
+ {
+ *pbNone = bool_(bMyNone);
+ }
+
+ return dwResult;
+}
+
+void OptionsCtrl::setDateTime(HANDLE hDateTime, DWORD dwDateTime)
+{
+ SendMessage(m_hOptWnd, OCM_SETDATETIME, reinterpret_cast<WPARAM>(hDateTime), dwDateTime);
+}
+
+
+ext::string OptionsCtrl::getDateTimeStr(HANDLE hDateTime)
+{
+ bool bNone = false;
+ DWORD dwTimestamp = getDateTime(hDateTime, &bNone);
+
+ return bNone ? muT("") : utils::formatDate(dwTimestamp);
+}
+
+void OptionsCtrl::setDateTimeStr(HANDLE hDateTime, const ext::string& strDateTime)
+{
+ if (strDateTime.empty())
+ {
+ setDateTimeNone(hDateTime);
+ }
+ else
+ {
+ setDateTime(hDateTime, utils::parseDate(strDateTime));
+ }
+}
+
+COLORREF OptionsCtrl::getColor(HANDLE hColor)
+{
+ return SendMessage(m_hOptWnd, OCM_GETITEMCOLOR, reinterpret_cast<WPARAM>(hColor), 0);
+}
+
+void OptionsCtrl::setColor(HANDLE hColor, COLORREF crColor)
+{
+ SendMessage(m_hOptWnd, OCM_SETITEMCOLOR, reinterpret_cast<WPARAM>(hColor), crColor);
+}
diff --git a/plugins/HistoryStats/src/optionsctrl.h b/plugins/HistoryStats/src/optionsctrl.h
new file mode 100644
index 0000000000..40ec04df0e
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrl.h
@@ -0,0 +1,89 @@
+#if !defined(HISTORYSTATS_GUARD_OPTIONSCTRL_H)
+#define HISTORYSTATS_GUARD_OPTIONSCTRL_H
+
+#include "_globals.h"
+#include "optionsctrldefs.h"
+
+/*
+ * OptionsCtrl
+ */
+
+class OptionsCtrl
+ : public OptionsCtrlDefs
+ , private pattern::NotCopyable<OptionsCtrl>
+{
+public:
+ typedef HANDLE Item;
+ typedef HANDLE Group;
+ typedef HANDLE Check;
+ typedef HANDLE Radio;
+ typedef HANDLE Edit;
+ typedef HANDLE Combo;
+ typedef HANDLE Button;
+ typedef HANDLE DateTime;
+ typedef HANDLE Color;
+
+private:
+ HWND m_hOptWnd;
+
+private:
+ explicit OptionsCtrl(const OptionsCtrl& other); // no implementation
+ const OptionsCtrl& operator =(const OptionsCtrl& other); // no implementation
+
+public:
+ explicit OptionsCtrl(HWND hOptWnd = NULL) : m_hOptWnd(hOptWnd) { }
+ ~OptionsCtrl() { }
+
+public:
+ const OptionsCtrl& operator <<(HWND hOptWnd) { m_hOptWnd = hOptWnd; return *this; }
+ operator HWND() { return m_hOptWnd; }
+
+public:
+ HANDLE insertGroup(HANDLE hParent, const mu_text* szLabel, DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertCheck(HANDLE hParent, const mu_text* szLabel, DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertRadio(HANDLE hParent, HANDLE hSibling, const mu_text* szLabel, DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertEdit(HANDLE hParent, const mu_text* szLabel, const mu_text* szEdit = muT(""), DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertCombo(HANDLE hParent, const mu_text* szLabel, DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertButton(HANDLE hParent, const mu_text* szLabel, const mu_text* szButton, DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertDateTime(HANDLE hParent, const mu_text* szLabel, DWORD dwDateTime, const mu_text* szFormat = muT("%Y-%m-%d"), DWORD dwFlags = 0, DWORD dwData = 0);
+ HANDLE insertColor(HANDLE hParent, const mu_text* szLabel, COLORREF crColor = 0, DWORD dwFlags = 0, DWORD dwData = 0);
+ const mu_text* getItemLabel(HANDLE hItem);
+ void setItemLabel(HANDLE hItem, const mu_text* szLabel);
+ bool isItemEnabled(HANDLE hItem);
+ void enableItem(HANDLE hItem, bool bEnable);
+ DWORD getItemData(HANDLE hItem);
+ void setItemData(HANDLE hItem, DWORD dwData);
+ bool isItemChecked(HANDLE hItem);
+ void checkItem(HANDLE hItem, bool bCheck);
+ int getRadioChecked(HANDLE hRadio);
+ void setRadioChecked(HANDLE hRadio, int nCheck);
+ int getEditNumber(HANDLE hEdit);
+ void setEditNumber(HANDLE hEdit, int nNumber);
+ const mu_text* getEditString(HANDLE hEdit);
+ void setEditString(HANDLE hEdit, const mu_text* szString);
+ void addComboItem(HANDLE hCombo, const mu_text* szItem);
+ int getComboSelected(HANDLE hCombo);
+ void setComboSelected(HANDLE hCombo, int nSelect);
+ void ensureVisible(HANDLE hItem);
+ void deleteAllItems();
+ HANDLE getSelection();
+ void selectItem(HANDLE hItem);
+ HANDLE getFirstItem();
+ HANDLE getNextItem(HANDLE hItem);
+ HANDLE getPrevItem(HANDLE hItem);
+ void setRedraw(bool bRedraw);
+ void deleteItem(HANDLE hItem);
+ void moveItem(HANDLE& hItem, HANDLE hInsertAfter);
+ int getScrollPos(int nBar);
+ void setScrollPos(int nBar, int nPos);
+ bool isDateTimeNone(HANDLE hDateTime);
+ void setDateTimeNone(HANDLE hDateTime);
+ DWORD getDateTime(HANDLE hDateTime, bool* pbNone = NULL);
+ void setDateTime(HANDLE hDateTime, DWORD dwDateTime);
+ ext::string getDateTimeStr(HANDLE hDateTime);
+ void setDateTimeStr(HANDLE hDateTime, const ext::string& strDateTime);
+ COLORREF getColor(HANDLE hColor);
+ void setColor(HANDLE hColor, COLORREF crColor);
+};
+
+#endif // HISTORYSTATS_GUARD_OPTIONSCTRL_H
diff --git a/plugins/HistoryStats/src/optionsctrldefs.h b/plugins/HistoryStats/src/optionsctrldefs.h
new file mode 100644
index 0000000000..0bc50b1b18
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrldefs.h
@@ -0,0 +1,160 @@
+#if !defined(HISTORYSTATS_GUARD_OPTIONSCTRLDEFS_H)
+#define HISTORYSTATS_GUARD_OPTIONSCTRLDEFS_H
+
+#include "_globals.h"
+
+/*
+ * OptionsCtrlDefs
+ */
+
+class OptionsCtrlDefs
+{
+public:
+ enum Message {
+ OCM_INSERTGROUP = WM_USER + 0, // (HANDLE hParent, OCGROUP* pGroup) -> HANDLE hGroup [Group]
+ OCM_INSERTCHECK = WM_USER + 1, // (HANDLE hParent, OCCHECK* pCheck) -> HANDLE hCheck [Check]
+ OCM_INSERTRADIO = WM_USER + 2, // (HANDLE hParent, OCRADIO* pRadio) -> HANDLE hRadio [Radio]
+ OCM_INSERTEDIT = WM_USER + 3, // (HANDLE hParent, OCEDIT* pEdit) -> HANDLE hEdit [Edit]
+ OCM_INSERTCOMBO = WM_USER + 4, // (HANDLE hParent, OCCOMBO* pCombo) -> HANDLE hCombo [Combo]
+ OCM_INSERTBUTTON = WM_USER + 5, // (HANDLE hParent, OCBUTTON* pButton) -> HANDLE hButton [Button]
+ OCM_GETITEMLABEL = WM_USER + 6, // (HANDLE hItem, #) -> const mu_text* szLabel
+ OCM_SETITEMLABEL = WM_USER + 7, // (HANDLE hItem, const mu_text* szLabel) -> #
+ OCM_ISITEMENABLED = WM_USER + 8, // (HANDLE hItem, #) -> BOOL bEnabled
+ OCM_ENABLEITEM = WM_USER + 9, // (HANDLE hItem, BOOL bEnable) -> #
+ OCM_GETITEMDATA = WM_USER + 10, // (HANDLE hItem, #) -> DWORD dwData
+ OCM_SETITEMDATA = WM_USER + 11, // (HANDLE hItem, DWORD dwData) -> #
+ OCM_ISITEMCHECKED = WM_USER + 12, // (HANDLE hItem, #) -> BOOL bChecked [Check/Radio]
+ OCM_CHECKITEM = WM_USER + 13, // (HANDLE hItem, BOOL bCheck) -> # [Check/Radio (ignores bCheck)]
+ OCM_GETRADIOCHECKED = WM_USER + 14, // (HANDLE hRadio, #) -> int nChecked [Radio]
+ OCM_SETRADIOCHECKED = WM_USER + 15, // (HANDLE hRadio, int nCheck) -> # [Radio]
+ OCM_GETEDITNUMBER = WM_USER + 16, // (HANDLE hEdit, #) -> int nNumber [Edit]
+ OCM_SETEDITNUMBER = WM_USER + 17, // (HANDLE hEdit, int nNumber) -> # [Edit]
+ OCM_GETEDITSTRING = WM_USER + 18, // (HANDLE hEdit, #) -> const mu_text* szString [Edit]
+ OCM_SETEDITSTRING = WM_USER + 19, // (HANDLE hEdit, const mu_text* szString) -> # [Edit]
+ OCM_ADDCOMBOITEM = WM_USER + 20, // (HANDLE hCombo, const mu_text* szItem) -> # [Combo]
+ OCM_GETCOMBOSELECTED = WM_USER + 21, // (HANDLE hCombo, #) -> int nSelected [Combo]
+ OCM_SETCOMBOSELECTED = WM_USER + 22, // (HANDLE hCombo, int nSelect) -> # [Combo]
+ OCM_ENSUREVISIBLE = WM_USER + 23, // (HANDLE hItem, #) -> #
+ OCM_DELETEALLITEMS = WM_USER + 24, // (#, #) -> #
+ OCM_GETSELECTION = WM_USER + 25, // (#, #) -> HANDLE hItem
+ OCM_SELECTITEM = WM_USER + 26, // (HANDLE hItem, #) -> #
+ OCM_GETITEM = WM_USER + 27, // (HANDLE hItem, DWORD dwFlag = OCGI_*) -> HANDLE hItem
+ OCM_DELETEITEM = WM_USER + 28, // (HANDLE hItem, #) -> #
+ OCM_MOVEITEM = WM_USER + 29, // (HANDLE* phItem, HANDLE hInsertAfter) -> #
+ OCM_GETSCROLLPOS = WM_USER + 30, // (int nBar, #) -> int nPos
+ OCM_SETSCROLLPOS = WM_USER + 31, // (int nBar, int nPos) -> #
+ OCM_INSERTDATETIME = WM_USER + 32, // (HANDLE hParent, OCDATETIME* pDateTime) -> HANDLE hDateTime [DateTime]
+ OCM_ISDATETIMENONE = WM_USER + 33, // (HANDLE hDateTime, #) -> BOOL bNone [DateTime]
+ OCM_SETDATETIMENONE = WM_USER + 34, // (HANDLE hDateTime, #) -> # [DateTime]
+ OCM_GETDATETIME = WM_USER + 35, // (HANDLE hDateTime, BOOL* pbNone) -> DWORD dwDateTime [DateTime]
+ OCM_SETDATETIME = WM_USER + 36, // (HANDLE hDateTime, DWORD dwDateTime) -> # [DateTime]
+ OCM_INSERTCOLOR = WM_USER + 37, // (HANDLE hParent, OCCOLOR* pColor) -> HANDLE hColor [Color]
+ OCM_GETITEMCOLOR = WM_USER + 38, // (HANDLE hColor, #) -> COLORREF crColor [Color]
+ OCM_SETITEMCOLOR = WM_USER + 39, // (HANDLE hColor, COLORREF crColor) -> # [Color]
+ };
+
+ enum Notification {
+ OCN_MODIFIED = NM_LAST - 1, // -> NMOPTIONSCTRL (hItem/dwData -> item that caused the modification ) [Check/Radio/Edit/Combo/DateTime/Color]
+ OCN_CLICKED = NM_LAST - 2, // -> NMOPTIONSCTRL (hItem/dwData -> item that was clicked ) [Button]
+ OCN_SELCHANGING = NM_LAST - 3, // -> NMOPTIONSCTRL (hItem/dwData -> item that gets unselected, may be NULL)
+ OCN_SELCHANGED = NM_LAST - 4, // -> NMOPTIONSCTRL (hItem/dwData -> item that got selected, may be NULL )
+ OCN_ITEMDROPPED = NM_LAST - 5, // -> NMOPTIONSCTRLDROP (hItem/dwData/hDropTarget/dwDropTargetData/bAbove )
+ };
+
+ enum StyleFlags {
+ OCS_ALLOWDRAGDROP = 0x0001,
+ };
+
+ enum ItemFlags {
+ OCF_BOLD = 0x01,
+ OCF_DISABLED = 0x02,
+ OCF_NODISABLECHILDS = 0x04,
+ OCF_CHECKED = 0x08, // [Check/Radio]
+ OCF_NONE = 0x08, // [DateTime]
+ OCF_DISABLECHILDSONUNCHECK = 0x10, // [Check/Radio]
+ OCF_DISABLECHILDSONINDEX0 = 0x10, // [Combo]
+ OCF_DRAWLINE = 0x10, // [Group]
+ OCF_NUMBER = 0x10, // [Edit]
+ OCF_DISABLECHILDSONNONE = 0x10, // [DateTime]
+ OCF_ALLOWNONE = 0x20, // [DateTime]
+ // combined flags
+ OCF_ROOTGROUP = OCF_BOLD | OCF_DRAWLINE, // [Group]
+ };
+
+ enum GetItemFlag {
+ OCGI_FIRST = 0, // hItem is ignored
+ OCGI_NEXT = 1, // next sibling relative to hItem
+ OCGI_PREV = 2, // previous sibling relative to hItem
+ };
+
+ struct OCGROUP {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ };
+
+ struct OCCHECK {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ };
+
+ struct OCRADIO {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ HANDLE hSibling;
+ };
+
+ struct OCEDIT {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ mu_text* szEdit;
+ };
+
+ struct OCCOMBO {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ };
+
+ struct OCBUTTON {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ mu_text* szButton;
+ };
+
+ struct OCDATETIME {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ mu_text* szFormat;
+ DWORD dwDateTime;
+ };
+
+ struct OCCOLOR {
+ DWORD dwFlags;
+ mu_text* szLabel;
+ DWORD dwData;
+ COLORREF crColor;
+ };
+
+ struct NMOPTIONSCTRL {
+ NMHDR hdr;
+ HANDLE hItem;
+ DWORD dwData;
+ };
+
+ struct NMOPTIONSCTRLDROP {
+ NMHDR hdr;
+ HANDLE hItem;
+ DWORD dwData;
+ HANDLE hDropTarget;
+ DWORD dwDropTargetData;
+ BOOL bAbove;
+ };
+};
+
+#endif // HISTORYSTATS_GUARD_OPTIONSCTRLDEFS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/optionsctrlimpl.cpp b/plugins/HistoryStats/src/optionsctrlimpl.cpp
new file mode 100644
index 0000000000..46d63e8c58
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl.cpp
@@ -0,0 +1,1428 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+#include "main.h"
+#include "iconlib.h"
+
+/*
+ * OptionsCtrlImpl
+ */
+
+const mu_text* OptionsCtrlImpl::m_ClassName = muT("HistoryStatsOptions");
+HIMAGELIST OptionsCtrlImpl::m_hStateIcons = NULL;
+int OptionsCtrlImpl::m_nStateIconsRef = 0;
+
+LRESULT CALLBACK OptionsCtrlImpl::staticWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ OptionsCtrlImpl* pCtrl = reinterpret_cast<OptionsCtrlImpl*>(GetWindowLong(hWnd, 0));
+
+ switch (msg)
+ {
+ case WM_NCCREATE:
+ {
+ CREATESTRUCT* pCS = reinterpret_cast<CREATESTRUCT*>(lParam);
+
+ pCS->style &= ~(WS_BORDER | WS_HSCROLL | WS_VSCROLL);
+ pCS->style |= WS_CHILD;
+ pCS->dwExStyle &= ~(WS_EX_CLIENTEDGE | WS_EX_STATICEDGE | WS_EX_WINDOWEDGE);
+
+ pCtrl = new OptionsCtrlImpl(hWnd, reinterpret_cast<int>(pCS->hMenu));
+ SetWindowLong(hWnd, 0, reinterpret_cast<LONG>(pCtrl));
+
+ return pCtrl ? TRUE : FALSE;
+ }
+
+ case WM_CREATE:
+ return pCtrl->onWMCreate(reinterpret_cast<CREATESTRUCT*>(lParam));
+
+ case WM_DESTROY:
+ pCtrl->onWMDestroy();
+ delete pCtrl;
+ SetWindowLong(hWnd, 0, 0);
+ return 0;
+
+ case WM_SETFOCUS:
+ SetFocus(pCtrl->m_hTree);
+ return 0;
+
+ case WM_ENABLE:
+ EnableWindow(pCtrl->m_hTree, wParam);
+ return 0;
+
+ case WM_GETFONT:
+ return SendMessage(pCtrl->m_hTree, WM_GETFONT, wParam, lParam);
+
+ case WM_SETFONT:
+ return SendMessage(pCtrl->m_hTree, WM_SETFONT, wParam, lParam);
+
+ case WM_WINDOWPOSCHANGED:
+ {
+ WINDOWPOS* pWP = reinterpret_cast<WINDOWPOS*>(lParam);
+
+ SetWindowPos(pCtrl->m_hTree, NULL, 0, 0, pWP->cx, pWP->cy, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+
+ case WM_SETREDRAW:
+ return SendMessage(pCtrl->m_hTree, WM_SETREDRAW, wParam, lParam);
+
+ case WM_NOTIFY:
+ {
+ NMHDR* pNM = reinterpret_cast<NMHDR*>(lParam);
+
+ if (pNM->hwndFrom == pCtrl->m_hTree)
+ {
+ switch (pNM->code)
+ {
+ case NM_CLICK:
+ pCtrl->onNMClick();
+ return 0;
+
+ case NM_CUSTOMDRAW:
+ return pCtrl->onNMCustomDraw(reinterpret_cast<NMTVCUSTOMDRAW*>(pNM));
+
+ case NM_KILLFOCUS:
+ pCtrl->onNMKillFocus();
+ return 0;
+
+ case TVN_ITEMEXPANDING:
+ return pCtrl->onTVNItemExpanding(reinterpret_cast<NMTREEVIEW*>(pNM));
+
+ case TVN_DELETEITEM:
+ pCtrl->onTVNDeleteItem(reinterpret_cast<NMTREEVIEW*>(pNM));
+ return 0;
+
+ case TVN_SELCHANGING:
+ pCtrl->onTVNSelChanging(reinterpret_cast<NMTREEVIEW*>(pNM));
+ return 0;
+
+ case TVN_SELCHANGED:
+ pCtrl->onTVNSelChanged(reinterpret_cast<NMTREEVIEW*>(pNM));
+ return 0;
+
+ case TVN_BEGINDRAG:
+ pCtrl->onTVNBeginDrag(reinterpret_cast<NMTREEVIEW*>(pNM));
+ return 0;
+ }
+ }
+ }
+ break;
+
+ case WM_MOUSEMOVE:
+ {
+ POINTS pts = MAKEPOINTS(lParam);
+ POINT pt = { pts.x, pts.y };
+
+ pCtrl->onWMMouseMove(pt);
+
+ return 0;
+ }
+
+ case WM_LBUTTONUP:
+ {
+ POINTS pts = MAKEPOINTS(lParam);
+ POINT pt = { pts.x, pts.y };
+
+ pCtrl->onWMLButtonUp(pt);
+
+ return 0;
+ }
+
+ case OCM_INSERTGROUP:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertGroup(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCGROUP*>(lParam)));
+
+ case OCM_INSERTCHECK:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertCheck(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCCHECK*>(lParam)));
+
+ case OCM_INSERTRADIO:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertRadio(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCRADIO*>(lParam)));
+
+ case OCM_INSERTEDIT:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertEdit(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCEDIT*>(lParam)));
+
+ case OCM_INSERTCOMBO:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertCombo(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCCOMBO*>(lParam)));
+
+ case OCM_INSERTBUTTON:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertButton(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCBUTTON*>(lParam)));
+
+ case OCM_GETITEMLABEL:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMGetItemLabel(reinterpret_cast<HTREEITEM>(wParam)));
+
+ case OCM_SETITEMLABEL:
+ pCtrl->onOCMSetItemLabel(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<const mu_text*>(lParam));
+ return 0;
+
+ case OCM_ISITEMENABLED:
+ return BOOL_(pCtrl->onOCMIsItemEnabled(reinterpret_cast<HTREEITEM>(wParam)));
+
+ case OCM_ENABLEITEM:
+ pCtrl->onOCMEnableItem(reinterpret_cast<HTREEITEM>(wParam), bool_(lParam));
+ return 0;
+
+ case OCM_GETITEMDATA:
+ return pCtrl->onOCMGetItemData(reinterpret_cast<HTREEITEM>(wParam));
+
+ case OCM_SETITEMDATA:
+ pCtrl->onOCMSetItemData(reinterpret_cast<HTREEITEM>(wParam), lParam);
+ return 0;
+
+ case OCM_ISITEMCHECKED:
+ return BOOL_(pCtrl->onOCMIsItemChecked(reinterpret_cast<HTREEITEM>(wParam)));
+
+ case OCM_CHECKITEM:
+ pCtrl->onOCMCheckItem(reinterpret_cast<HTREEITEM>(wParam), bool_(lParam));
+ return 0;
+
+ case OCM_GETRADIOCHECKED:
+ return pCtrl->onOCMGetRadioChecked(reinterpret_cast<HTREEITEM>(wParam));
+
+ case OCM_SETRADIOCHECKED:
+ pCtrl->onOCMSetRadioChecked(reinterpret_cast<HTREEITEM>(wParam), lParam);
+ return 0;
+
+ case OCM_GETEDITNUMBER:
+ return pCtrl->onOCMGetEditNumber(reinterpret_cast<HTREEITEM>(wParam));
+
+ case OCM_SETEDITNUMBER:
+ pCtrl->onOCMSetEditNumber(reinterpret_cast<HTREEITEM>(wParam), lParam);
+ return 0;
+
+ case OCM_GETEDITSTRING:
+ return reinterpret_cast<LPARAM>(pCtrl->onOCMGetEditString(reinterpret_cast<HTREEITEM>(wParam)));
+
+ case OCM_SETEDITSTRING:
+ pCtrl->onOCMSetEditString(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<const mu_text*>(lParam));
+ return 0;
+
+ case OCM_ADDCOMBOITEM:
+ pCtrl->onOCMAddComboItem(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<const mu_text*>(lParam));
+ return 0;
+
+ case OCM_GETCOMBOSELECTED:
+ return pCtrl->onOCMGetComboSelected(reinterpret_cast<HTREEITEM>(wParam));
+
+ case OCM_SETCOMBOSELECTED:
+ pCtrl->onOCMSetComboSelected(reinterpret_cast<HTREEITEM>(wParam), lParam);
+ return 0;
+
+ case OCM_ENSUREVISIBLE:
+ pCtrl->onOCMEnsureVisible(reinterpret_cast<HTREEITEM>(wParam));
+ return 0;
+
+ case OCM_DELETEALLITEMS:
+ pCtrl->onOCMDeleteAllItems();
+ return 0;
+
+ case OCM_GETSELECTION:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMGetSelection());
+
+ case OCM_SELECTITEM:
+ pCtrl->onOCMSelectItem(reinterpret_cast<HTREEITEM>(wParam));
+ return 0;
+
+ case OCM_GETITEM:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMGetItem(reinterpret_cast<HTREEITEM>(wParam), lParam));
+
+ case OCM_DELETEITEM:
+ pCtrl->onOCMDeleteItem(reinterpret_cast<HTREEITEM>(wParam));
+ return 0;
+
+ case OCM_MOVEITEM:
+ pCtrl->onOCMMoveItem(*reinterpret_cast<HTREEITEM*>(wParam), reinterpret_cast<HTREEITEM>(lParam));
+ return 0;
+
+ case OCM_GETSCROLLPOS:
+ return GetScrollPos(pCtrl->m_hTree, wParam);
+
+ case OCM_SETSCROLLPOS:
+ SetScrollPos(pCtrl->m_hTree, wParam, lParam, TRUE);
+ return 0;
+
+ case OCM_INSERTDATETIME:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertDateTime(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCDATETIME*>(lParam)));
+
+ case OCM_ISDATETIMENONE:
+ return BOOL_(pCtrl->onOCMIsDateTimeNone(reinterpret_cast<HTREEITEM>(wParam)));
+
+ case OCM_SETDATETIMENONE:
+ pCtrl->onOCMSetDateTimeNone(reinterpret_cast<HTREEITEM>(wParam));
+ return 0;
+
+ case OCM_GETDATETIME:
+ return pCtrl->onOCMGetDateTime(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<BOOL*>(lParam));
+
+ case OCM_SETDATETIME:
+ pCtrl->onOCMSetDateTime(reinterpret_cast<HTREEITEM>(wParam), lParam);
+ return 0;
+
+ case OCM_INSERTCOLOR:
+ return reinterpret_cast<LRESULT>(pCtrl->onOCMInsertColor(reinterpret_cast<HTREEITEM>(wParam), reinterpret_cast<OCCOLOR*>(lParam)));
+
+ case OCM_GETITEMCOLOR:
+ return pCtrl->onOCMGetItemColor(reinterpret_cast<HTREEITEM>(wParam));
+
+ case OCM_SETITEMCOLOR:
+ pCtrl->onOCMSetItemColor(reinterpret_cast<HTREEITEM>(wParam), lParam);
+ return 0;
+ }
+
+ return DefWindowProc(hWnd, msg, wParam, lParam);
+}
+
+LRESULT CALLBACK OptionsCtrlImpl::staticTreeProc(HWND hTree, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ OptionsCtrlImpl* pCtrl = reinterpret_cast<OptionsCtrlImpl*>(GetWindowLong(GetParent(hTree), 0));
+
+ switch (msg)
+ {
+ case WM_LBUTTONDOWN:
+ pCtrl->onTreeWMLButtonDown(wParam, MAKEPOINTS(lParam));
+
+ case WM_CHAR:
+ pCtrl->onTreeWMChar(wParam, LOWORD(lParam), HIWORD(lParam));
+ break;
+
+ case WM_KEYDOWN:
+ pCtrl->onTreeWMKeyDown(wParam, LOWORD(lParam), HIWORD(lParam));
+ break;
+
+ case WM_VSCROLL:
+ case WM_HSCROLL:
+ case WM_MOUSEWHEEL:
+ case WM_WINDOWPOSCHANGED:
+ pCtrl->onTreeXScroll(); // stops editing
+ break;
+
+ case WM_COMMAND:
+ {
+ switch (LOWORD(wParam))
+ {
+ case ccEdit:
+ if (HIWORD(wParam) == EN_CHANGE)
+ {
+ pCtrl->onENChange();
+ }
+ else if (HIWORD(wParam) == EN_KILLFOCUS)
+ {
+ pCtrl->onNMKillFocus();
+ }
+ return 0;
+
+ case ccCombo:
+ if (HIWORD(wParam) == CBN_SELCHANGE)
+ {
+ pCtrl->onCBNSelChange();
+ }
+ else if (HIWORD(wParam) == CBN_KILLFOCUS)
+ {
+ pCtrl->onNMKillFocus();
+ }
+ return 0;
+
+ case ccButton:
+ if (HIWORD(wParam) == BN_CLICKED)
+ {
+ pCtrl->onBNClicked();
+ }
+ else if (HIWORD(wParam) == BN_KILLFOCUS)
+ {
+ pCtrl->onNMKillFocus();
+ }
+ return 0;
+
+ case ccColor:
+ if (HIWORD(wParam) == CPN_COLOURCHANGED)
+ {
+ pCtrl->onCPNColorChanged();
+ }
+ return 0;
+ }
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+ NMHDR* pNM = reinterpret_cast<NMHDR*>(lParam);
+
+ if (pNM->idFrom == ccDateTime)
+ {
+ if (pNM->code == DTN_DATETIMECHANGE)
+ {
+ pCtrl->onDTNDateTimeChange();
+ }
+ else if (pNM->code == NM_KILLFOCUS)
+ {
+ pCtrl->onNMKillFocus();
+ }
+ return 0;
+ }
+ }
+ break;
+ }
+
+ return CallWindowProc(pCtrl->m_pfnOldTreeProc, hTree, msg, wParam, lParam);
+}
+
+void OptionsCtrlImpl::staticInitStateImages()
+{
+ if (m_nStateIconsRef++ == 0 && !m_hStateIcons)
+ {
+ m_hStateIcons = ImageList_Create(OS::smIconCX(), OS::smIconCY(), OS::imageListColor() | ILC_MASK, 16, 0);
+
+ staticUpdateStateImages(0);
+ IconLib::registerCallback(staticUpdateStateImages, 0);
+ }
+}
+
+void OptionsCtrlImpl::staticFreeStateImages()
+{
+ if (m_hStateIcons && --m_nStateIconsRef == 0)
+ {
+ IconLib::unregisterCallback(staticUpdateStateImages, 0);
+ ImageList_Destroy(m_hStateIcons);
+
+ m_hStateIcons = NULL;
+ }
+}
+
+void OptionsCtrlImpl::staticUpdateStateImages(LPARAM lParam)
+{
+ static const IconLib::IconIndex StateIcons[18] = {
+ IconLib::iiTreeCheck1,
+ IconLib::iiTreeCheck2,
+ IconLib::iiTreeCheck3,
+ IconLib::iiTreeCheck4,
+ IconLib::iiTreeRadio1,
+ IconLib::iiTreeRadio2,
+ IconLib::iiTreeRadio3,
+ IconLib::iiTreeRadio4,
+ IconLib::iiTreeEdit1,
+ IconLib::iiTreeEdit2,
+ IconLib::iiTreeCombo1,
+ IconLib::iiTreeCombo2,
+ IconLib::iiTreeFolder1,
+ IconLib::iiTreeFolder2,
+ IconLib::iiTreeButton1,
+ IconLib::iiTreeButton2,
+ IconLib::iiTreeDateTime1,
+ IconLib::iiTreeDateTime2,
+ };
+
+ ImageList_RemoveAll(m_hStateIcons);
+
+ array_each_(i, StateIcons)
+ {
+ ImageList_AddIcon(m_hStateIcons, IconLib::getIcon(StateIcons[i]));
+ }
+}
+
+bool OptionsCtrlImpl::registerClass()
+{
+ const WNDCLASSEX wcx = {
+ sizeof(wcx), // cbSize
+ 0, // style
+ staticWndProc, // lpfnWndProc
+ 0, // cbClsExtra
+ sizeof(OptionsCtrlImpl*), // cbWndExtra
+ g_hInst, // hInstance
+ NULL, // hIcon
+ NULL, // hCursor
+ NULL, // hbrBackground
+ NULL, // lpszMenuName
+ m_ClassName, // lpszClassName
+ NULL // hIconSm
+ };
+
+ if (!RegisterClassEx(&wcx))
+ {
+ return false;
+ }
+
+ INITCOMMONCONTROLSEX icc;
+
+ icc.dwSize = sizeof(icc);
+ icc.dwICC = ICC_DATE_CLASSES;
+
+ if (!InitCommonControlsEx(&icc))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+void OptionsCtrlImpl::unregisterClass()
+{
+ UnregisterClass(m_ClassName, g_hInst);
+}
+
+OptionsCtrlImpl::OptionsCtrlImpl(HWND hWnd, int nOwnId)
+ : m_hWnd(hWnd), m_nOwnId(nOwnId), m_hTree(NULL), m_pfnOldTreeProc(NULL), m_bModified(true), m_hDragItem(NULL)
+{
+}
+
+OptionsCtrlImpl::~OptionsCtrlImpl()
+{
+}
+
+LRESULT OptionsCtrlImpl::onWMCreate(CREATESTRUCT* pCS)
+{
+ DWORD dwStyle = 0;
+
+ if (!(pCS->style & OCS_ALLOWDRAGDROP))
+ {
+ dwStyle |= TVS_DISABLEDRAGDROP;
+ }
+
+ m_hTree = CreateWindowEx(
+ WS_EX_CLIENTEDGE,
+ WC_TREEVIEW,
+ muT(""),
+ WS_CHILD | WS_VISIBLE | WS_TABSTOP | TVS_HASLINES | TVS_NOHSCROLL | TVS_SHOWSELALWAYS | dwStyle,
+ 0,
+ 0,
+ pCS->cx,
+ pCS->cy,
+ m_hWnd,
+ reinterpret_cast<HMENU>(ccTree),
+ g_hInst,
+ NULL);
+
+ if (!m_hTree)
+ {
+ return -1;
+ }
+
+ // subclass tree view
+ m_pfnOldTreeProc = reinterpret_cast<WNDPROC>(SetWindowLong(m_hTree, GWLP_WNDPROC, reinterpret_cast<LONG>(staticTreeProc)));
+
+ // create/set state icons
+ staticInitStateImages();
+ TreeView_SetImageList(m_hTree, m_hStateIcons, TVSIL_NORMAL);
+
+ return 0;
+}
+
+void OptionsCtrlImpl::onWMDestroy()
+{
+ // empty tree just to be sure
+ onOCMDeleteAllItems();
+
+ // unset/free state icons
+ if (TreeView_GetImageList(m_hTree, TVSIL_NORMAL))
+ {
+ staticFreeStateImages();
+ }
+
+ // undo subclassing of tree view
+ SetWindowLong(m_hTree, GWLP_WNDPROC, reinterpret_cast<LONG>(m_pfnOldTreeProc));
+ m_pfnOldTreeProc = NULL;
+
+ // destroy tree view before invalidating 'this'
+ DestroyWindow(m_hTree);
+ m_hTree = NULL;
+}
+
+void OptionsCtrlImpl::onNMClick()
+{
+ DWORD dwPoint = GetMessagePos();
+ POINTS pts = MAKEPOINTS(dwPoint);
+ TVHITTESTINFO hti = { { pts.x, pts.y } };
+
+ ScreenToClient(m_hTree, &hti.pt);
+
+ HTREEITEM hSelItem = TreeView_GetSelection(m_hTree);
+ HTREEITEM hItem = TreeView_HitTest(m_hTree, &hti);
+
+ if (hItem == hSelItem && hti.flags & TVHT_ONITEM)
+ {
+ getItem(hItem)->onSelect();
+ }
+}
+
+LRESULT OptionsCtrlImpl::onNMCustomDraw(NMTVCUSTOMDRAW* pNMCustomDraw)
+{
+ switch (pNMCustomDraw->nmcd.dwDrawStage)
+ {
+ case CDDS_PREPAINT:
+ return CDRF_NOTIFYITEMDRAW;
+
+ case CDDS_ITEMPREPAINT:
+ {
+ Item* pItem = reinterpret_cast<Item*>(pNMCustomDraw->nmcd.lItemlParam);
+
+ if (pItem)
+ {
+ /*
+ if (pItem->m_ItemType == itGroup && reinterpret_cast<Group*>(pItem)->m_bDrawLine)
+ {
+ COLORREF crNewBk = GetSysColor(COLOR_HIGHLIGHT);
+ COLORREF crOldBk = SetBkColor(pNMCustomDraw->nmcd.hdc, crNewBk);
+
+ RECT rLine = pNMCustomDraw->nmcd.rc;
+ RECT rText;
+
+ TreeView_GetItemRect(m_hTree, reinterpret_cast<HTREEITEM>(pNMCustomDraw->nmcd.dwItemSpec), &rText, TRUE);
+
+ rLine.top = (rLine.bottom = (rLine.bottom + rLine.top) / 2) - 1;
+ rLine.left = rText.right;
+ rLine.right -= 2;
+
+ ExtTextOut(pNMCustomDraw->nmcd.hdc, 0, 0, ETO_OPAQUE, &rLine, NULL, 0, NULL);
+
+ OffsetRect(&rLine, 0, 2);
+ ExtTextOut(pNMCustomDraw->nmcd.hdc, 0, 0, ETO_OPAQUE, &rLine, NULL, 0, NULL);
+
+ SetBkColor(pNMCustomDraw->nmcd.hdc, crOldBk);
+ }
+ */
+
+ /*
+ if (pItem->m_ItemType == itColor)
+ {
+ COLORREF crColor = reinterpret_cast<Color*>(pItem)->m_crColor;
+
+ COLORREF crNewBk = GetSysColor(COLOR_HIGHLIGHT);
+ COLORREF crOldBk = SetBkColor(pNMCustomDraw->nmcd.hdc, crNewBk);
+
+ RECT rColor = pNMCustomDraw->nmcd.rc;
+ RECT rText;
+
+ TreeView_GetItemRect(m_hTree, reinterpret_cast<HTREEITEM>(pNMCustomDraw->nmcd.dwItemSpec), &rText, TRUE);
+
+ ++rColor.top;
+ --rColor.bottom;
+ rColor.left = rText.right;
+ rColor.right -= 2;
+
+ if (rColor.right - 40 > rColor.left)
+ {
+ rColor.right = rColor.left + 40;
+ }
+
+ ExtTextOut(pNMCustomDraw->nmcd.hdc, 0, 0, ETO_OPAQUE, &rColor, NULL, 0, NULL);
+
+ SetBkColor(pNMCustomDraw->nmcd.hdc, crColor);
+
+ InflateRect(&rColor, -1, -1);
+ ExtTextOut(pNMCustomDraw->nmcd.hdc, 0, 0, ETO_OPAQUE, &rColor, NULL, 0, NULL);
+
+ SetBkColor(pNMCustomDraw->nmcd.hdc, crOldBk);
+ }
+ */
+
+ if (!pItem->m_bEnabled)
+ {
+ pNMCustomDraw->clrText = GetSysColor(COLOR_GRAYTEXT);
+ }
+ }
+ }
+ return CDRF_NEWFONT;
+ }
+
+ return 0;
+}
+
+void OptionsCtrlImpl::onNMKillFocus()
+{
+ HWND hWndFocused = GetFocus();
+
+ if (hWndFocused != m_hTree && !IsChild(m_hTree, hWndFocused))
+ {
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected)
+ {
+ Item* pItem = getItem(hSelected);
+
+ if (pItem->m_ItemType == itDateTime)
+ {
+ // MEMO: handle date/time picker (with possibly open month calendar) separately
+ DateTime* pDateTime = reinterpret_cast<DateTime*>(pItem);
+
+ if (!pDateTime->isMonthCalVisible())
+ {
+ pDateTime->onDeselect();
+ }
+ }
+ else if (pItem->m_ItemType == itColor)
+ {
+ // MEMO: handle color picker separately
+ }
+ else
+ {
+ pItem->onDeselect();
+ }
+ }
+ }
+}
+
+LRESULT OptionsCtrlImpl::onTVNItemExpanding(NMTREEVIEW* pNMTreeView)
+{
+ return BOOL_(
+ pNMTreeView->action == TVE_COLLAPSE || pNMTreeView->action == TVE_COLLAPSERESET ||
+ (pNMTreeView->action == TVE_TOGGLE && pNMTreeView->itemNew.state & TVIS_EXPANDED));
+}
+
+void OptionsCtrlImpl::onTVNDeleteItem(NMTREEVIEW* pNMTreeView)
+{
+ if (pNMTreeView->itemOld.hItem)
+ {
+ // fake OCN_SELCHANGING message
+ NMOPTIONSCTRL nmoc;
+
+ nmoc.hdr.code = OCN_SELCHANGING;
+ nmoc.hdr.hwndFrom = m_hWnd;
+ nmoc.hdr.idFrom = m_nOwnId;
+ nmoc.hItem = 0;
+ nmoc.dwData = 0;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmoc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmoc));
+
+ // fake OCN_SELCHANGED message
+ nmoc.hdr.code = OCN_SELCHANGED;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmoc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmoc));
+
+ // do actual delete
+ Item* pItem = getItem(pNMTreeView->itemOld.hItem);
+ setItem(pNMTreeView->itemOld.hItem, NULL);
+
+ pItem->onDeselect();
+
+ if (--pItem->m_nRef == 0)
+ {
+ delete pItem;
+ }
+ }
+}
+
+void OptionsCtrlImpl::onTVNSelChanging(NMTREEVIEW* pNMTreeView)
+{
+ NMOPTIONSCTRL nmoc;
+
+ nmoc.hdr.code = OCN_SELCHANGING;
+ nmoc.hdr.hwndFrom = m_hWnd;
+ nmoc.hdr.idFrom = m_nOwnId;
+ nmoc.hItem = reinterpret_cast<HANDLE>(pNMTreeView->itemOld.hItem);
+ nmoc.dwData = pNMTreeView->itemOld.hItem ? reinterpret_cast<Item*>(pNMTreeView->itemOld.lParam)->m_dwData : 0;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmoc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmoc));
+
+ if (pNMTreeView->itemOld.hItem)
+ {
+ reinterpret_cast<Item*>(pNMTreeView->itemOld.lParam)->onDeselect();
+ }
+}
+
+void OptionsCtrlImpl::onTVNSelChanged(NMTREEVIEW* pNMTreeView)
+{
+ if (pNMTreeView->itemNew.hItem)
+ {
+ reinterpret_cast<Item*>(pNMTreeView->itemNew.lParam)->onSelect();
+ }
+
+ NMOPTIONSCTRL nmoc;
+
+ nmoc.hdr.code = OCN_SELCHANGED;
+ nmoc.hdr.hwndFrom = m_hWnd;
+ nmoc.hdr.idFrom = m_nOwnId;
+ nmoc.hItem = reinterpret_cast<HANDLE>(pNMTreeView->itemNew.hItem);
+ nmoc.dwData = pNMTreeView->itemNew.hItem ? reinterpret_cast<Item*>(pNMTreeView->itemNew.lParam)->m_dwData : 0;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmoc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmoc));
+}
+
+void OptionsCtrlImpl::onTVNBeginDrag(NMTREEVIEW* pNMTreeView)
+{
+ SetCapture(m_hWnd);
+ m_hDragItem = pNMTreeView->itemNew.hItem;
+ TreeView_SelectItem(m_hTree, m_hDragItem);
+}
+
+void OptionsCtrlImpl::onWMMouseMove(const POINT& pt)
+{
+ if (!m_hDragItem)
+ {
+ return;
+ }
+
+ TVHITTESTINFO hti;
+
+ hti.pt = pt;
+ ClientToScreen(m_hWnd, &hti.pt);
+ ScreenToClient(m_hTree, &hti.pt);
+ TreeView_HitTest(m_hTree, &hti);
+
+ if (hti.flags & (TVHT_ONITEM | TVHT_ONITEMRIGHT))
+ {
+ RECT rItem;
+
+ TreeView_GetItemRect(m_hTree, hti.hItem, &rItem, FALSE);
+ TreeView_SetInsertMark(m_hTree, hti.hItem, (hti.pt.y >= (rItem.top + rItem.bottom) / 2) ? TRUE : FALSE);
+ }
+ else
+ {
+ if (hti.flags & TVHT_ABOVE)
+ {
+ SendMessage(m_hTree, WM_VSCROLL, MAKEWPARAM(SB_LINEUP, 0), 0);
+ }
+ else if (hti.flags & TVHT_BELOW)
+ {
+ SendMessage(m_hTree, WM_VSCROLL, MAKELPARAM(SB_LINEDOWN, 0), 0);
+ }
+
+ TreeView_SetInsertMark(m_hTree, NULL, FALSE);
+ }
+}
+
+void OptionsCtrlImpl::onWMLButtonUp(const POINT& pt)
+{
+ if (!m_hDragItem)
+ {
+ return;
+ }
+
+ // revert to noraml state
+ HTREEITEM hDragItem = m_hDragItem;
+
+ TreeView_SetInsertMark(m_hTree, NULL, FALSE);
+ m_hDragItem = NULL;
+ ReleaseCapture();
+
+ // check for drop target and handle
+ TVHITTESTINFO hti;
+
+ hti.pt = pt;
+ ClientToScreen(m_hWnd, &hti.pt);
+ ScreenToClient(m_hTree, &hti.pt);
+ TreeView_HitTest(m_hTree, &hti);
+
+ if (hti.flags & (TVHT_ONITEM | TVHT_ONITEMRIGHT))
+ {
+ RECT rItem;
+
+ TreeView_GetItemRect(m_hTree, hti.hItem, &rItem, FALSE);
+
+ bool bAbove = (hti.pt.y >= (rItem.top + rItem.bottom) / 2);
+
+ NMOPTIONSCTRLDROP nmocd;
+
+ nmocd.hdr.code = OCN_ITEMDROPPED;
+ nmocd.hdr.hwndFrom = m_hWnd;
+ nmocd.hdr.idFrom = m_nOwnId;
+ nmocd.hItem = reinterpret_cast<HANDLE>(hDragItem);
+ nmocd.dwData = hDragItem ? getItem(hDragItem)->m_dwData : 0;
+ nmocd.hDropTarget = reinterpret_cast<HANDLE>(hti.hItem);
+ nmocd.dwDropTargetData = hti.hItem ? getItem(hti.hItem)->m_dwData : 0;
+ nmocd.bAbove = BOOL_(bAbove);
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmocd.hdr.idFrom, reinterpret_cast<LPARAM>(&nmocd));
+ }
+}
+
+void OptionsCtrlImpl::onENChange()
+{
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected)
+ {
+ setModified(getItem(hSelected));
+ }
+}
+
+void OptionsCtrlImpl::onCBNSelChange()
+{
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected)
+ {
+ Item* pItem = getItem(hSelected);
+
+ assert(pItem->m_ItemType == itCombo);
+
+ reinterpret_cast<Combo*>(pItem)->onSelChanged();
+ setModified(pItem);
+ }
+}
+
+void OptionsCtrlImpl::onBNClicked()
+{
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected)
+ {
+ Item* pItem = getItem(hSelected);
+
+ assert(pItem->m_ItemType == itButton);
+
+ NMOPTIONSCTRL nmoc;
+
+ nmoc.hdr.code = OCN_CLICKED;
+ nmoc.hdr.hwndFrom = m_hWnd;
+ nmoc.hdr.idFrom = m_nOwnId;
+ nmoc.hItem = reinterpret_cast<HANDLE>(pItem->m_hItem);
+ nmoc.dwData = pItem->m_dwData;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmoc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmoc));
+ }
+}
+
+void OptionsCtrlImpl::onDTNDateTimeChange()
+{
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected)
+ {
+ Item* pItem = getItem(hSelected);
+
+ assert(pItem->m_ItemType == itDateTime);
+
+ reinterpret_cast<DateTime*>(pItem)->onDateTimeChange();
+ setModified(pItem);
+ }
+}
+
+void OptionsCtrlImpl::onCPNColorChanged()
+{
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected)
+ {
+ Item* pItem = getItem(hSelected);
+
+ assert(pItem->m_ItemType == itColor);
+
+ reinterpret_cast<Color*>(pItem)->onColorChange();
+ setModified(pItem);
+ }
+}
+
+void OptionsCtrlImpl::onTreeWMLButtonDown(UINT nFlags, POINTS point)
+{
+ TVHITTESTINFO hti = { { point.x, point.y } };
+
+ if (TreeView_HitTest(m_hTree, &hti) && hti.flags & TVHT_ONITEMICON)
+ {
+ getItem(hti.hItem)->onToggle();
+ }
+}
+
+void OptionsCtrlImpl::onTreeWMChar(UINT nChar, UINT nRepCnt, UINT nFlags)
+{
+ if (nChar == VK_SPACE)
+ {
+ HTREEITEM hItem = TreeView_GetSelection(m_hTree);
+
+ if (hItem)
+ {
+ getItem(hItem)->onToggle();
+ }
+ }
+}
+
+void OptionsCtrlImpl::onTreeWMKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
+{
+ if (nChar == VK_RIGHT)
+ {
+ HTREEITEM hItem = TreeView_GetSelection(m_hTree);
+
+ if (hItem)
+ {
+ getItem(hItem)->onActivate();
+ }
+ }
+}
+
+void OptionsCtrlImpl::onTreeXScroll()
+{
+ HTREEITEM hSelected = TreeView_GetSelection(m_hTree);
+
+ if (hSelected && isItemValid(hSelected))
+ {
+ getItem(hSelected)->onDeselect();
+ }
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertGroup(HTREEITEM hParent, OCGROUP* pGroup)
+{
+ assert(pGroup);
+ assert(pGroup->szLabel);
+
+ return (new Group(this, hParent ? getItem(hParent) : NULL, pGroup->szLabel, pGroup->dwFlags, pGroup->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertCheck(HTREEITEM hParent, OCCHECK* pCheck)
+{
+ assert(pCheck);
+ assert(pCheck->szLabel);
+
+ return (new Check(this, hParent ? getItem(hParent) : NULL, pCheck->szLabel, pCheck->dwFlags, pCheck->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertRadio(HTREEITEM hParent, OCRADIO* pRadio)
+{
+ assert(pRadio);
+ assert(pRadio->szLabel);
+
+ Item* pSibling = pRadio->hSibling ? getItem(reinterpret_cast<HTREEITEM>(pRadio->hSibling)) : NULL;
+
+ assert(!pSibling || pSibling->m_ItemType == itRadio);
+
+ return (new Radio(this, hParent ? getItem(hParent) : NULL, reinterpret_cast<Radio*>(pSibling), pRadio->szLabel, pRadio->dwFlags, pRadio->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertEdit(HTREEITEM hParent, OCEDIT* pEdit)
+{
+ assert(pEdit);
+ assert(pEdit->szLabel);
+ assert(pEdit->szEdit);
+
+ return (new Edit(this, hParent ? getItem(hParent) : NULL, pEdit->szLabel, pEdit->szEdit, pEdit->dwFlags, pEdit->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertCombo(HTREEITEM hParent, OCCOMBO* pCombo)
+{
+ assert(pCombo);
+ assert(pCombo->szLabel);
+
+ return (new Combo(this, hParent ? getItem(hParent) : NULL, pCombo->szLabel, pCombo->dwFlags, pCombo->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertButton(HTREEITEM hParent, OCBUTTON* pButton)
+{
+ assert(pButton);
+ assert(pButton->szLabel);
+ assert(pButton->szButton);
+
+ return (new Button(this, hParent ? getItem(hParent) : NULL, pButton->szLabel, pButton->szButton, pButton->dwFlags, pButton->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertDateTime(HTREEITEM hParent, OCDATETIME* pDateTime)
+{
+ assert(pDateTime);
+ assert(pDateTime->szLabel);
+ assert(pDateTime->szFormat);
+
+ return (new DateTime(this, hParent ? getItem(hParent) : NULL, pDateTime->szLabel, pDateTime->szFormat, pDateTime->dwDateTime, pDateTime->dwFlags, pDateTime->dwData))->m_hItem;
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMInsertColor(HTREEITEM hParent, OCCOLOR* pColor)
+{
+ assert(pColor);
+ assert(pColor->szLabel);
+
+ return (new Color(this, hParent ? getItem(hParent) : NULL, pColor->szLabel, pColor->crColor, pColor->dwFlags, pColor->dwData))->m_hItem;
+}
+
+const mu_text* OptionsCtrlImpl::onOCMGetItemLabel(HTREEITEM hItem)
+{
+ return getItem(hItem)->getLabel();
+}
+
+void OptionsCtrlImpl::onOCMSetItemLabel(HTREEITEM hItem, const mu_text* szLabel)
+{
+ assert(szLabel);
+
+ getItem(hItem)->setLabel(szLabel);
+}
+
+bool OptionsCtrlImpl::onOCMIsItemEnabled(HTREEITEM hItem)
+{
+ return getItem(hItem)->m_bEnabled;
+}
+
+void OptionsCtrlImpl::onOCMEnableItem(HTREEITEM hItem, bool bEnable)
+{
+ getItem(hItem)->setEnabled(bEnable);
+}
+
+DWORD OptionsCtrlImpl::onOCMGetItemData(HTREEITEM hItem)
+{
+ return getItem(hItem)->m_dwData;
+}
+
+void OptionsCtrlImpl::onOCMSetItemData(HTREEITEM hItem, DWORD dwData)
+{
+ getItem(hItem)->m_dwData = dwData;
+}
+
+bool OptionsCtrlImpl::onOCMIsItemChecked(HTREEITEM hItem)
+{
+ Item* pItem = getItem(hItem);
+
+ assert(pItem->m_ItemType == itCheck || pItem->m_ItemType == itRadio);
+
+ return (pItem->m_ItemType == itCheck) ? reinterpret_cast<Check*>(pItem)->isChecked() : reinterpret_cast<Radio*>(pItem)->isChecked();
+}
+
+void OptionsCtrlImpl::onOCMCheckItem(HTREEITEM hItem, bool bCheck)
+{
+ Item* pItem = getItem(hItem);
+
+ assert(pItem->m_ItemType == itCheck || pItem->m_ItemType == itRadio);
+
+ if (pItem->m_ItemType == itCheck)
+ {
+ reinterpret_cast<Check*>(pItem)->setChecked(bCheck);
+ }
+ else
+ {
+ reinterpret_cast<Radio*>(pItem)->setChecked();
+ }
+}
+
+int OptionsCtrlImpl::onOCMGetRadioChecked(HTREEITEM hRadio)
+{
+ Item* pRadio = getItem(hRadio);
+
+ assert(pRadio->m_ItemType == itRadio);
+
+ return reinterpret_cast<Radio*>(pRadio)->m_pSiblings->getChecked();
+}
+
+void OptionsCtrlImpl::onOCMSetRadioChecked(HTREEITEM hRadio, int nCheck)
+{
+ Item* pRadio = getItem(hRadio);
+
+ assert(pRadio->m_ItemType == itRadio);
+
+ reinterpret_cast<Radio*>(pRadio)->m_pSiblings->setChecked(nCheck);
+}
+
+int OptionsCtrlImpl::onOCMGetEditNumber(HTREEITEM hEdit)
+{
+ Item* pEdit = getItem(hEdit);
+
+ assert(pEdit->m_ItemType == itEdit);
+
+ return reinterpret_cast<Edit*>(pEdit)->getNumber();
+}
+
+void OptionsCtrlImpl::onOCMSetEditNumber(HTREEITEM hEdit, int nNumber)
+{
+ Item* pEdit = getItem(hEdit);
+
+ assert(pEdit->m_ItemType == itEdit);
+
+ reinterpret_cast<Edit*>(pEdit)->setNumber(nNumber);
+}
+
+const mu_text* OptionsCtrlImpl::onOCMGetEditString(HTREEITEM hEdit)
+{
+ Item* pEdit = getItem(hEdit);
+
+ assert(pEdit->m_ItemType == itEdit);
+
+ return reinterpret_cast<Edit*>(pEdit)->getString();
+}
+
+void OptionsCtrlImpl::onOCMSetEditString(HTREEITEM hEdit, const mu_text *szString)
+{
+ assert(szString);
+
+ Item* pEdit = getItem(hEdit);
+
+ assert(pEdit->m_ItemType == itEdit);
+
+ reinterpret_cast<Edit*>(pEdit)->setString(szString);
+}
+
+void OptionsCtrlImpl::onOCMAddComboItem(HTREEITEM hCombo, const mu_text* szItem)
+{
+ assert(szItem);
+
+ Item* pCombo = getItem(hCombo);
+
+ assert(pCombo->m_ItemType == itCombo);
+
+ reinterpret_cast<Combo*>(pCombo)->addItem(szItem);
+}
+
+int OptionsCtrlImpl::onOCMGetComboSelected(HTREEITEM hCombo)
+{
+ Item* pCombo = getItem(hCombo);
+
+ assert(pCombo->m_ItemType == itCombo);
+
+ return reinterpret_cast<Combo*>(pCombo)->getSelected();
+}
+
+void OptionsCtrlImpl::onOCMSetComboSelected(HTREEITEM hCombo, int nSelect)
+{
+ Item* pCombo = getItem(hCombo);
+
+ assert(pCombo->m_ItemType == itCombo);
+
+ reinterpret_cast<Combo*>(pCombo)->setSelected(nSelect);
+}
+
+void OptionsCtrlImpl::onOCMEnsureVisible(HTREEITEM hItem)
+{
+ if (!hItem)
+ {
+ hItem = TreeView_GetChild(m_hTree, TVI_ROOT);
+ }
+
+ if (hItem)
+ {
+ TreeView_EnsureVisible(m_hTree, hItem);
+ }
+}
+
+void OptionsCtrlImpl::onOCMDeleteAllItems()
+{
+ TreeView_SelectItem(m_hTree, NULL);
+ TreeView_DeleteAllItems(m_hTree);
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMGetSelection()
+{
+ return TreeView_GetSelection(m_hTree);
+}
+
+void OptionsCtrlImpl::onOCMSelectItem(HTREEITEM hItem)
+{
+ TreeView_SelectItem(m_hTree, hItem);
+}
+
+HTREEITEM OptionsCtrlImpl::onOCMGetItem(HTREEITEM hItem, DWORD dwFlag)
+{
+ switch (dwFlag)
+ {
+ case OCGI_FIRST:
+ return TreeView_GetRoot(m_hTree);
+
+ case OCGI_NEXT:
+ return TreeView_GetNextSibling(m_hTree, hItem);
+
+ case OCGI_PREV:
+ return TreeView_GetPrevSibling(m_hTree, hItem);
+
+ default:
+ return NULL;
+ }
+}
+
+void OptionsCtrlImpl::onOCMDeleteItem(HTREEITEM hItem)
+{
+ TreeView_DeleteItem(m_hTree, hItem);
+}
+
+void OptionsCtrlImpl::onOCMMoveItem(HTREEITEM& hItem, HTREEITEM hInsertAfter)
+{
+ assert(hItem);
+
+ TVINSERTSTRUCT tvis;
+
+ tvis.hParent = TreeView_GetParent(m_hTree, hItem);
+ tvis.hInsertAfter = hInsertAfter ? hInsertAfter : TVI_FIRST;
+ tvis.item.hItem = hItem;
+ tvis.item.mask = TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_STATE | TVIF_PARAM;
+ tvis.item.stateMask = TVIS_BOLD | TVIS_EXPANDED;
+
+ if (TreeView_GetItem(m_hTree, &tvis.item))
+ {
+ Item* pItem = reinterpret_cast<Item*>(tvis.item.lParam);
+
+ ++pItem->m_nRef;
+
+ TreeView_DeleteItem(m_hTree, hItem);
+
+ tvis.item.mask |= TVIF_TEXT;
+ tvis.item.pszText = muT("");
+
+ hItem = TreeView_InsertItem(m_hTree, &tvis);
+
+ ext::string strLabel = pItem->getLabel();
+
+ pItem->m_hItem = hItem;
+ pItem->setLabel(strLabel.c_str());
+ }
+}
+
+bool OptionsCtrlImpl::onOCMIsDateTimeNone(HTREEITEM hDateTime)
+{
+ Item* pDateTime = getItem(hDateTime);
+
+ assert(pDateTime->m_ItemType == itDateTime);
+
+ return reinterpret_cast<DateTime*>(pDateTime)->isNone();
+}
+
+void OptionsCtrlImpl::onOCMSetDateTimeNone(HTREEITEM hDateTime)
+{
+ Item* pDateTime = getItem(hDateTime);
+
+ assert(pDateTime->m_ItemType == itDateTime);
+
+ reinterpret_cast<DateTime*>(pDateTime)->setNone();
+}
+
+DWORD OptionsCtrlImpl::onOCMGetDateTime(HTREEITEM hDateTime, BOOL* pbNone)
+{
+ Item* pDateTime = getItem(hDateTime);
+
+ assert(pDateTime->m_ItemType == itDateTime);
+ assert(pbNone);
+
+ *pbNone = reinterpret_cast<DateTime*>(pDateTime)->isNone();
+
+ return reinterpret_cast<DateTime*>(pDateTime)->getTimestamp();
+}
+
+void OptionsCtrlImpl::onOCMSetDateTime(HTREEITEM hDateTime, DWORD dwTimestamp)
+{
+ Item* pDateTime = getItem(hDateTime);
+
+ assert(pDateTime->m_ItemType == itDateTime);
+
+ reinterpret_cast<DateTime*>(pDateTime)->setTimestamp(dwTimestamp);
+}
+
+COLORREF OptionsCtrlImpl::onOCMGetItemColor(HTREEITEM hColor)
+{
+ Item* pColor = getItem(hColor);
+
+ assert(pColor->m_ItemType == itColor);
+
+ return reinterpret_cast<Color*>(pColor)->getColor();
+}
+
+void OptionsCtrlImpl::onOCMSetItemColor(HTREEITEM hColor, COLORREF crColor)
+{
+ Item* pColor = getItem(hColor);
+
+ assert(pColor->m_ItemType == itColor);
+
+ reinterpret_cast<Color*>(pColor)->setColor(crColor);
+}
+
+void OptionsCtrlImpl::insertItem(Item* pParent, Item* pItem, const mu_text* szNodeText, DWORD dwFlags, int iImage)
+{
+ assert(!pParent || pParent->m_hItem);
+ assert(pItem);
+ assert(szNodeText);
+
+ TVINSERTSTRUCT tvis;
+
+ tvis.hParent = pParent ? pParent->m_hItem : TVI_ROOT;
+ tvis.hInsertAfter = TVI_LAST;
+ tvis.item.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_STATE | TVIF_PARAM;
+ tvis.item.pszText = const_cast<mu_text*>(szNodeText);
+ tvis.item.iImage = iImage;
+ tvis.item.iSelectedImage = iImage;
+ tvis.item.stateMask = TVIS_EXPANDED | ((dwFlags & OCF_BOLD) ? TVIS_BOLD : 0);
+ tvis.item.state = tvis.item.stateMask;
+ tvis.item.lParam = reinterpret_cast<LPARAM>(pItem);
+
+ pItem->m_hItem = TreeView_InsertItem(m_hTree, &tvis);
+}
+
+void OptionsCtrlImpl::setModified(Item* pItem, bool bModified /* = true */)
+{
+ m_bModified = bModified;
+
+ if (bModified)
+ {
+ NMOPTIONSCTRL nmoc;
+
+ nmoc.hdr.code = OCN_MODIFIED;
+ nmoc.hdr.hwndFrom = m_hWnd;
+ nmoc.hdr.idFrom = m_nOwnId;
+ nmoc.hItem = reinterpret_cast<HANDLE>(pItem->m_hItem);
+ nmoc.dwData = pItem->m_dwData;
+
+ SendMessage(GetParent(m_hWnd), WM_NOTIFY, nmoc.hdr.idFrom, reinterpret_cast<LPARAM>(&nmoc));
+ }
+}
+
+bool OptionsCtrlImpl::isItemValid(HTREEITEM hItem)
+{
+ assert(hItem);
+
+ TVITEM tvi;
+
+ tvi.mask = TVIF_HANDLE | TVIF_PARAM;
+ tvi.hItem = hItem;
+
+ Item* pItem = TreeView_GetItem(m_hTree, &tvi) ? reinterpret_cast<Item*>(tvi.lParam) : NULL;
+
+ return bool_(pItem);
+}
+
+OptionsCtrlImpl::Item* OptionsCtrlImpl::getItem(HTREEITEM hItem)
+{
+ assert(hItem);
+
+ TVITEM tvi;
+
+ tvi.mask = TVIF_HANDLE | TVIF_PARAM;
+ tvi.hItem = hItem;
+
+ Item* pItem = TreeView_GetItem(m_hTree, &tvi) ? reinterpret_cast<Item*>(tvi.lParam) : NULL;
+
+ assert(pItem);
+
+ return pItem;
+}
+
+void OptionsCtrlImpl::setItem(HTREEITEM hItem, Item* pItem)
+{
+ TVITEM tvi;
+
+ tvi.mask = TVIF_HANDLE | TVIF_PARAM;
+ tvi.hItem = hItem;
+ tvi.lParam = reinterpret_cast<LPARAM>(pItem);
+
+ TreeView_SetItem(m_hTree, &tvi);
+}
+
+void OptionsCtrlImpl::setStateImage(HTREEITEM hItem, int iImage)
+{
+ assert(hItem);
+
+ TVITEM tvi;
+
+ tvi.mask = TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
+ tvi.hItem = hItem;
+ tvi.iImage = iImage;
+ tvi.iSelectedImage = iImage;
+
+ TreeView_SetItem(m_hTree, &tvi);
+}
+
+void OptionsCtrlImpl::setNodeText(HTREEITEM hItem, const mu_text* szNodeText)
+{
+ assert(hItem);
+ assert(szNodeText);
+
+ TVITEM tvi;
+
+ tvi.mask = TVIF_HANDLE | TVIF_TEXT;
+ tvi.hItem = hItem;
+ tvi.pszText = const_cast<mu_text*>(szNodeText);
+
+ TreeView_SetItem(m_hTree, &tvi);
+}
+
+bool OptionsCtrlImpl::getItemFreeRect(HTREEITEM hItem, RECT& outRect)
+{
+ RECT rLine, rText;
+
+ if (TreeView_GetItemRect(m_hTree, hItem, &rLine, FALSE) && TreeView_GetItemRect(m_hTree, hItem, &rText, TRUE))
+ {
+ outRect.left = rText.right + 2;
+ outRect.top = rText.top;
+ outRect.right = rLine.right - 2;
+ outRect.bottom = rText.bottom;
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl.h b/plugins/HistoryStats/src/optionsctrlimpl.h
new file mode 100644
index 0000000000..43313ac195
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl.h
@@ -0,0 +1,447 @@
+#if !defined(HISTORYSTATS_GUARD_OPTIONSCTRLIMPL_H)
+#define HISTORYSTATS_GUARD_OPTIONSCTRLIMPL_H
+
+#include "_globals.h"
+#include "optionsctrldefs.h"
+
+#include <vector>
+
+#include "utils.h"
+
+/*
+ * OptionsCtrlImpl
+ */
+
+class OptionsCtrlImpl
+ : public OptionsCtrlDefs
+ , private pattern::NotCopyable<OptionsCtrlImpl>
+{
+private:
+ enum ItemType {
+ itGroup,
+ itCheck,
+ itRadio,
+ itEdit,
+ itCombo,
+ itButton,
+ itDateTime,
+ itColor,
+ };
+
+ enum StateIcon {
+ // C = checked
+ // U = unchecked
+ // G = greyed
+ siCheckU = 0,
+ siCheckC = 1,
+ siCheckUG = 2,
+ siCheckCG = 3,
+ siRadioU = 4,
+ siRadioC = 5,
+ siRadioUG = 6,
+ siRadioCG = 7,
+ siEdit = 8,
+ siEditG = 9,
+ siCombo = 10,
+ siComboG = 11,
+ siFolder = 12,
+ siFolderG = 13,
+ siButton = 14,
+ siButtonG = 15,
+ siDateTime = 16,
+ siDateTimeG = 17,
+ siColor = 0,
+ siColorG = 2,
+ };
+
+ enum ChildControl {
+ ccTree = 100,
+ ccEdit = 101,
+ ccCombo = 102,
+ ccButton = 103,
+ ccDateTime = 104,
+ ccColor = 105,
+ };
+
+ class Item
+ {
+ public:
+ OptionsCtrlImpl* m_pCtrl;
+ HTREEITEM m_hItem;
+ int m_nRef;
+ ItemType m_ItemType;
+ bool m_bEnabled;
+ bool m_bDisableChilds;
+ ext::string m_strLabel;
+ DWORD m_dwData;
+
+ protected:
+ explicit Item(OptionsCtrlImpl* pCtrl, ItemType ItemType, const mu_text* szLabel, DWORD dwFlags, DWORD dwData);
+
+ void enableChilds(bool bEnable);
+
+ public:
+ virtual ~Item() { }
+
+ virtual void onToggle() { }
+ virtual void onSelect() { }
+ virtual void onDeselect() { }
+ virtual void onActivate() { }
+
+ virtual void setEnabled(bool bEnable) = 0;
+ virtual void childAdded(Item* pChild) = 0;
+
+ virtual void setLabel(const mu_text* szLabel);
+ virtual const mu_text* getLabel() { return m_strLabel.c_str(); }
+ };
+
+ class Group
+ : public Item
+ {
+ public:
+ bool m_bDrawLine;
+
+ public:
+ explicit Group(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, DWORD dwFlags, DWORD dwData);
+
+ virtual void setEnabled(bool bEnable);
+ virtual void childAdded(Item* pChild);
+
+ };
+
+ class Check
+ : public Item
+ {
+ public:
+ bool m_bChecked;
+ bool m_bDisableChildsOnUncheck;
+
+ private:
+ int getStateImage();
+ bool getChildEnable();
+ void updateItem();
+
+ public:
+ explicit Check(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, DWORD dwFlags, DWORD dwData);
+
+ virtual void onToggle();
+
+ virtual void setEnabled(bool bEnable) { m_bEnabled = bEnable; updateItem(); }
+ virtual void childAdded(Item* pChild);
+
+ bool isChecked() { return m_bChecked; }
+ void setChecked(bool bCheck) { m_bChecked = bCheck; updateItem(); }
+ };
+
+ class Radio; // forward declaration for RadioSiblings
+
+ class RadioSiblings
+ {
+ friend class Radio; // for join()/leave()
+
+ public:
+ std::vector<Radio*> m_Radios;
+ int m_nRadios;
+ int m_nChecked;
+
+ private:
+ int join(Radio* pRadio);
+ bool leave(int nRadio);
+
+ public:
+ explicit RadioSiblings() : m_nRadios(0), m_nChecked(-1) { }
+
+ int getChecked() { return m_nChecked; }
+ bool isChecked(int nRadio) { return (m_nChecked == nRadio); }
+ void setChecked(int nRadio);
+ };
+
+ class Radio
+ : public Item
+ {
+ friend class RadioSiblings; // for updateItem()
+
+ public:
+ bool m_bDisableChildsOnUncheck;
+ int m_nIndex;
+ RadioSiblings* m_pSiblings;
+
+ private:
+ bool getChildEnable(bool bChecked);
+ void updateItem();
+
+ public:
+ explicit Radio(OptionsCtrlImpl* pCtrl, Item* pParent, Radio* pSibling, const mu_text* szLabel, DWORD dwFlags, DWORD dwData);
+ virtual ~Radio();
+
+ virtual void onToggle();
+
+ virtual void setEnabled(bool bEnable) { m_bEnabled = bEnable; updateItem(); }
+ virtual void childAdded(Item* pChild);
+
+ bool isChecked() { return m_pSiblings->isChecked(m_nIndex); }
+ void setChecked() { m_pSiblings->setChecked(m_nIndex); }
+ };
+
+ class Edit
+ : public Item
+ {
+ public:
+ bool m_bNumber;
+ ext::string m_strEdit;
+ HWND m_hEditWnd;
+
+ private:
+ ext::string getEditText();
+ ext::string getCombinedText();
+
+ public:
+ explicit Edit(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, const mu_text* szEdit, DWORD dwFlags, DWORD dwData);
+
+ virtual void onToggle() { onActivate(); }
+ virtual void onSelect();
+ virtual void onDeselect();
+ virtual void onActivate();
+
+ virtual void setEnabled(bool bEnable);
+ virtual void childAdded(Item* pChild);
+
+ virtual void setLabel(const mu_text* szLabel);
+
+ const mu_text* getString();
+ void setString(const mu_text* szString);
+ int getNumber() { return _ttoi(getString()); }
+ void setNumber(int nNumber) { setString(utils::intToString(nNumber).c_str()); }
+ };
+
+ class Combo
+ : public Item
+ {
+ public:
+ bool m_bDisableChildsOnIndex0;
+ int m_nSelected;
+ std::vector<ext::string> m_Items;
+ HWND m_hComboWnd;
+
+ private:
+ void enableChildsCombo();
+ bool getChildEnable();
+ int getComboSel();
+ ext::string getCombinedText();
+
+ public:
+ explicit Combo(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, DWORD dwFlags, DWORD dwData);
+
+ virtual void onToggle() { onActivate(); }
+ virtual void onSelect();
+ virtual void onDeselect();
+ virtual void onActivate();
+
+ virtual void setEnabled(bool bEnable);
+ virtual void childAdded(Item* pChild);
+
+ virtual void setLabel(const mu_text* szLabel);
+
+ void addItem(const mu_text* szItem);
+ int getSelected();
+ void setSelected(int nSelect);
+ void onSelChanged();
+ };
+
+ class Button
+ : public Item
+ {
+ public:
+ ext::string m_strButton;
+ HWND m_hButtonWnd;
+
+ public:
+ explicit Button(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, const mu_text* szButton, DWORD dwFlags, DWORD dwData);
+
+ virtual void onToggle() { onActivate(); }
+ virtual void onSelect();
+ virtual void onDeselect();
+ virtual void onActivate();
+
+ virtual void setEnabled(bool bEnable);
+ virtual void childAdded(Item* pChild);
+
+ virtual void setLabel(const mu_text* szLabel);
+ };
+
+ class DateTime
+ : public Item
+ {
+ public:
+ static ext::string getDTFormatString(const ext::string& strFormat);
+ static SYSTEMTIME toSystemTime(DWORD dwTimestamp);
+ static DWORD fromSystemTime(const SYSTEMTIME& st);
+
+ public:
+ bool m_bDisableChildsOnNone;
+ bool m_bAllowNone;
+ ext::string m_strFormat;
+ ext::string m_strFormatDT;
+ bool m_bNone;
+ DWORD m_dwTimestamp;
+ HWND m_hDateTimeWnd;
+
+ private:
+ void enableChildsDateTime();
+ bool getChildEnable();
+ DWORD getTimestampValue();
+ bool getTimestampNone();
+ ext::string getCombinedText();
+
+ public:
+ explicit DateTime(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, const mu_text* szFormat, DWORD dwTimestamp, DWORD dwFlags, DWORD dwData);
+
+ virtual void onToggle() { onActivate(); }
+ virtual void onSelect();
+ virtual void onDeselect();
+ virtual void onActivate();
+
+ virtual void setEnabled(bool bEnable);
+ virtual void childAdded(Item* pChild);
+
+ virtual void setLabel(const mu_text* szLabel);
+
+ bool isNone();
+ void setNone();
+ DWORD getTimestamp();
+ void setTimestamp(DWORD dwTimestamp);
+ void onDateTimeChange();
+ bool isMonthCalVisible();
+ };
+
+ class Color
+ : public Item
+ {
+ public:
+ COLORREF m_crColor;
+ HWND m_hColorWnd;
+
+ private:
+ COLORREF getColorValue();
+
+ public:
+ explicit Color(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, COLORREF crColor, DWORD dwFlags, DWORD dwData);
+
+ virtual void onToggle() { onActivate(); }
+ virtual void onSelect();
+ virtual void onDeselect();
+ virtual void onActivate();
+
+ virtual void setEnabled(bool bEnable);
+ virtual void childAdded(Item* pChild);
+
+ virtual void setLabel(const mu_text* szLabel);
+
+ COLORREF getColor();
+ void setColor(COLORREF crColor);
+ void onColorChange();
+ };
+
+private:
+ static const mu_text* m_ClassName;
+ static HIMAGELIST m_hStateIcons;
+ static int m_nStateIconsRef;
+
+private:
+ static LRESULT CALLBACK staticWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
+ static LRESULT CALLBACK staticTreeProc(HWND hTree, UINT msg, WPARAM wParam, LPARAM lParam);
+ static void staticInitStateImages();
+ static void staticFreeStateImages();
+ static void staticUpdateStateImages(LPARAM lParam);
+
+public:
+ static bool registerClass();
+ static void unregisterClass();
+
+private:
+ HWND m_hWnd;
+ int m_nOwnId;
+ HWND m_hTree;
+ WNDPROC m_pfnOldTreeProc;
+ bool m_bModified;
+ HTREEITEM m_hDragItem;
+
+private:
+ explicit OptionsCtrlImpl(HWND hWnd, int nOwnId);
+ explicit OptionsCtrlImpl(const OptionsCtrlImpl& other); // no implementation
+ const OptionsCtrlImpl& operator =(const OptionsCtrlImpl& other); // no implementation
+ ~OptionsCtrlImpl();
+
+private:
+ LRESULT onWMCreate(CREATESTRUCT* pCS);
+ void onWMDestroy();
+ void onNMClick();
+ LRESULT onNMCustomDraw(NMTVCUSTOMDRAW* pNMCustomDraw);
+ void onNMKillFocus();
+ LRESULT onTVNItemExpanding(NMTREEVIEW* pNMTreeView);
+ void onTVNDeleteItem(NMTREEVIEW* pNMTreeView);
+ void onTVNSelChanging(NMTREEVIEW* pNMTreeView);
+ void onTVNSelChanged(NMTREEVIEW* pNMTreeView);
+ void onTVNBeginDrag(NMTREEVIEW* pNMTreeView);
+ void onWMMouseMove(const POINT& pt);
+ void onWMLButtonUp(const POINT& pt);
+ void onENChange();
+ void onCBNSelChange();
+ void onBNClicked();
+ void onDTNDateTimeChange();
+ void onCPNColorChanged();
+ void onTreeWMLButtonDown(UINT nFlags, POINTS point);
+ void onTreeWMChar(UINT nChar, UINT nRepCnt, UINT nFlags);
+ void onTreeWMKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
+ void onTreeXScroll();
+ HTREEITEM onOCMInsertGroup(HTREEITEM hParent, OCGROUP* pGroup);
+ HTREEITEM onOCMInsertCheck(HTREEITEM hParent, OCCHECK* pCheck);
+ HTREEITEM onOCMInsertRadio(HTREEITEM hParent, OCRADIO* pRadio);
+ HTREEITEM onOCMInsertEdit(HTREEITEM hParent, OCEDIT* pEdit);
+ HTREEITEM onOCMInsertCombo(HTREEITEM hParent, OCCOMBO* pCombo);
+ HTREEITEM onOCMInsertButton(HTREEITEM hParent, OCBUTTON* pButton);
+ HTREEITEM onOCMInsertDateTime(HTREEITEM hParent, OCDATETIME* pDateTime);
+ HTREEITEM onOCMInsertColor(HTREEITEM hParent, OCCOLOR* pColor);
+ const mu_text* onOCMGetItemLabel(HTREEITEM hItem);
+ void onOCMSetItemLabel(HTREEITEM hItem, const mu_text* szLabel);
+ bool onOCMIsItemEnabled(HTREEITEM hItem);
+ void onOCMEnableItem(HTREEITEM hItem, bool bEnable);
+ DWORD onOCMGetItemData(HTREEITEM hItem);
+ void onOCMSetItemData(HTREEITEM hItem, DWORD dwData);
+ bool onOCMIsItemChecked(HTREEITEM hItem);
+ void onOCMCheckItem(HTREEITEM hItem, bool bCheck);
+ int onOCMGetRadioChecked(HTREEITEM hRadio);
+ void onOCMSetRadioChecked(HTREEITEM hRadio, int nCheck);
+ int onOCMGetEditNumber(HTREEITEM hEdit);
+ void onOCMSetEditNumber(HTREEITEM hEdit, int nNumber);
+ const mu_text* onOCMGetEditString(HTREEITEM hEdit);
+ void onOCMSetEditString(HTREEITEM hEdit, const mu_text *szString);
+ void onOCMAddComboItem(HTREEITEM hCombo, const mu_text* szItem);
+ int onOCMGetComboSelected(HTREEITEM hCombo);
+ void onOCMSetComboSelected(HTREEITEM hCombo, int nSelect);
+ void onOCMEnsureVisible(HTREEITEM hItem);
+ void onOCMDeleteAllItems();
+ HTREEITEM onOCMGetSelection();
+ void onOCMSelectItem(HTREEITEM hItem);
+ HTREEITEM onOCMGetItem(HTREEITEM hItem, DWORD dwFlag);
+ void onOCMDeleteItem(HTREEITEM hItem);
+ void onOCMMoveItem(HTREEITEM& hItem, HTREEITEM hInsertAfter);
+ bool onOCMIsDateTimeNone(HTREEITEM hDateTime);
+ void onOCMSetDateTimeNone(HTREEITEM hDateTime);
+ DWORD onOCMGetDateTime(HTREEITEM hDateTime, BOOL* pbNone);
+ void onOCMSetDateTime(HTREEITEM hDateTime, DWORD dwTimestamp);
+ COLORREF onOCMGetItemColor(HTREEITEM hColor);
+ void onOCMSetItemColor(HTREEITEM hColor, COLORREF crColor);
+
+private:
+ void insertItem(Item* pParent, Item* pItem, const mu_text* szNodeText, DWORD dwFlags, int iImage);
+ void setModified(Item* pItem, bool bModified = true);
+ bool isItemValid(HTREEITEM hItem);
+ Item* getItem(HTREEITEM hItem);
+ void setItem(HTREEITEM hItem, Item* pItem);
+ void setStateImage(HTREEITEM hItem, int iImage);
+ void setNodeText(HTREEITEM hItem, const mu_text* szNodeText);
+ bool getItemFreeRect(HTREEITEM hItem, RECT& outRect);
+};
+
+#endif // HISTORYSTATS_GUARD_OPTIONSCTRLIMPL_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_button.cpp b/plugins/HistoryStats/src/optionsctrlimpl_button.cpp
new file mode 100644
index 0000000000..ae1b8395ca
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_button.cpp
@@ -0,0 +1,125 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+#include "main.h"
+
+/*
+ * OptionsCtrlImpl::Button
+ */
+
+OptionsCtrlImpl::Button::Button(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, const mu_text* szButton, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itButton, szLabel, dwFlags, dwData)
+ , m_hButtonWnd(NULL)
+ , m_strButton(szButton)
+{
+ m_pCtrl->insertItem(pParent, this, m_strLabel.c_str(), dwFlags, m_bEnabled ? siButton : siButtonG);
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::Button::onSelect()
+{
+ if (!m_bEnabled || m_hButtonWnd)
+ {
+ return;
+ }
+
+ HFONT hTreeFront = reinterpret_cast<HFONT>(SendMessage(m_pCtrl->m_hTree, WM_GETFONT, 0, 0));
+ RECT r;
+
+ if (m_pCtrl->getItemFreeRect(m_hItem, r))
+ {
+ r.top -= 2;
+ r.bottom += 2;
+
+ if (r.left + 50 > r.right)
+ {
+ r.left = r.right - 50;
+ }
+
+ HWND hTempWnd;
+
+ DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON | BS_TEXT | BS_CENTER | BS_VCENTER;
+
+ if (hTempWnd = CreateWindowEx(
+ 0, WC_BUTTON, m_strButton.c_str(), dwStyle,
+ r.left, r.top, r.right - r.left, r.bottom - r.top,
+ m_pCtrl->m_hTree, reinterpret_cast<HMENU>(ccButton), g_hInst, NULL))
+ {
+ SendMessage(hTempWnd, WM_SETFONT, reinterpret_cast<WPARAM>(hTreeFront), MAKELPARAM(TRUE, 0));
+
+ m_hButtonWnd = hTempWnd;
+ }
+ }
+}
+
+void OptionsCtrlImpl::Button::onDeselect()
+{
+ if (m_hButtonWnd)
+ {
+ RECT rToInvalidate;
+ bool bValidRect = false;
+
+ if (GetWindowRect(m_hButtonWnd, &rToInvalidate))
+ {
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 0);
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 1);
+
+ bValidRect = true;
+ }
+
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+
+ DestroyWindow(m_hButtonWnd);
+ m_hButtonWnd = NULL;
+
+ InvalidateRect(m_pCtrl->m_hTree, bValidRect ? &rToInvalidate : NULL, TRUE);
+ }
+}
+
+void OptionsCtrlImpl::Button::onActivate()
+{
+ if (!m_hButtonWnd)
+ {
+ onSelect();
+ }
+
+ if (m_hButtonWnd)
+ {
+ SetFocus(m_hButtonWnd);
+ }
+}
+
+void OptionsCtrlImpl::Button::setEnabled(bool bEnable)
+{
+ m_bEnabled = bEnable;
+
+ m_pCtrl->setStateImage(m_hItem, bEnable ? siButton : siButtonG);
+
+ if (m_bDisableChilds)
+ {
+ enableChilds(m_bEnabled);
+ }
+}
+
+void OptionsCtrlImpl::Button::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds)
+ {
+ pChild->setEnabled(m_bEnabled);
+ }
+}
+
+void OptionsCtrlImpl::Button::setLabel(const mu_text* szLabel)
+{
+ m_strLabel = szLabel;
+
+ // only if not showing button (otherwise update when button disappears)
+ if (!m_hButtonWnd)
+ {
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+ }
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_check.cpp b/plugins/HistoryStats/src/optionsctrlimpl_check.cpp
new file mode 100644
index 0000000000..d6c2b99c56
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_check.cpp
@@ -0,0 +1,59 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+/*
+ * OptionsCtrlImpl::Check
+ */
+
+int OptionsCtrlImpl::Check::getStateImage()
+{
+ return m_bEnabled ? (m_bChecked ? siCheckC : siCheckU) : (m_bChecked ? siCheckCG : siCheckUG);
+}
+
+bool OptionsCtrlImpl::Check::getChildEnable()
+{
+ return
+ !m_bDisableChildsOnUncheck && m_bDisableChilds && m_bEnabled ||
+ m_bDisableChildsOnUncheck && m_bChecked && (!m_bDisableChilds || m_bEnabled);
+}
+
+void OptionsCtrlImpl::Check::updateItem()
+{
+ m_pCtrl->setStateImage(m_hItem, m_bEnabled ? (m_bChecked ? siCheckC : siCheckU) : (m_bChecked ? siCheckCG : siCheckUG));
+
+ if (m_bDisableChilds || m_bDisableChildsOnUncheck)
+ {
+ enableChilds(getChildEnable());
+ }
+}
+
+OptionsCtrlImpl::Check::Check(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itCheck, szLabel, dwFlags, dwData)
+{
+ m_bChecked = bool_(dwFlags & OCF_CHECKED);
+ m_bDisableChildsOnUncheck = bool_(dwFlags & OCF_DISABLECHILDSONUNCHECK);
+
+ pCtrl->insertItem(pParent, this, szLabel, dwFlags, getStateImage());
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::Check::onToggle()
+{
+ if (m_bEnabled)
+ {
+ setChecked(!m_bChecked);
+ m_pCtrl->setModified(this);
+ }
+}
+
+void OptionsCtrlImpl::Check::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds || m_bDisableChildsOnUncheck)
+ {
+ pChild->setEnabled(getChildEnable());
+ }
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_color.cpp b/plugins/HistoryStats/src/optionsctrlimpl_color.cpp
new file mode 100644
index 0000000000..fa9eebce2d
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_color.cpp
@@ -0,0 +1,163 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+#include "main.h"
+
+/*
+ * OptionsCtrlImpl::Color
+ */
+
+COLORREF OptionsCtrlImpl::Color::getColorValue()
+{
+ return SendMessage(m_hColorWnd, CPM_GETCOLOUR, 0, 0);
+}
+
+OptionsCtrlImpl::Color::Color(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, COLORREF crColor, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itColor, szLabel, dwFlags, dwData)
+ , m_hColorWnd(NULL)
+ , m_crColor(crColor)
+{
+ m_pCtrl->insertItem(pParent, this, m_strLabel.c_str(), dwFlags, m_bEnabled ? siColor : siColorG);
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::Color::onSelect()
+{
+ if (!m_bEnabled || m_hColorWnd)
+ {
+ return;
+ }
+
+ HFONT hTreeFront = reinterpret_cast<HFONT>(SendMessage(m_pCtrl->m_hTree, WM_GETFONT, 0, 0));
+ RECT r;
+
+ if (m_pCtrl->getItemFreeRect(m_hItem, r))
+ {
+ r.top -= 1;
+ r.bottom += 1;
+
+ if (r.left + 40 > r.right)
+ {
+ r.left = r.right - 40;
+ }
+
+ if (r.right - 40 > r.left)
+ {
+ r.right = r.left + 40;
+ }
+
+ HWND hTempWnd;
+
+ DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP;
+
+ if (hTempWnd = CreateWindowEx(
+ 0, _T(WNDCLASS_COLOURPICKER), NULL, dwStyle,
+ r.left, r.top, r.right - r.left, r.bottom - r.top,
+ m_pCtrl->m_hTree, reinterpret_cast<HMENU>(ccColor), g_hInst, NULL))
+ {
+ SendMessage(hTempWnd, CPM_SETCOLOUR, 0, m_crColor);
+
+ m_hColorWnd = hTempWnd;
+ }
+ }
+}
+
+void OptionsCtrlImpl::Color::onDeselect()
+{
+ if (m_hColorWnd)
+ {
+ RECT rToInvalidate;
+ bool bValidRect = false;
+
+ if (GetWindowRect(m_hColorWnd, &rToInvalidate))
+ {
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 0);
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 1);
+
+ bValidRect = true;
+ }
+
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+
+ DestroyWindow(m_hColorWnd);
+ m_hColorWnd = NULL;
+
+ InvalidateRect(m_pCtrl->m_hTree, bValidRect ? &rToInvalidate : NULL, TRUE);
+ }
+}
+
+void OptionsCtrlImpl::Color::onActivate()
+{
+ if (!m_hColorWnd)
+ {
+ onSelect();
+ }
+
+ if (m_hColorWnd)
+ {
+ SetFocus(m_hColorWnd);
+ }
+}
+
+void OptionsCtrlImpl::Color::onColorChange()
+{
+ if (m_hColorWnd)
+ {
+ m_crColor = getColorValue();
+ }
+}
+
+void OptionsCtrlImpl::Color::setEnabled(bool bEnable)
+{
+ m_bEnabled = bEnable;
+
+ m_pCtrl->setStateImage(m_hItem, bEnable ? siColor : siColorG);
+
+ if (m_bDisableChilds)
+ {
+ enableChilds(m_bEnabled);
+ }
+}
+
+void OptionsCtrlImpl::Color::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds)
+ {
+ pChild->setEnabled(m_bEnabled);
+ }
+}
+
+void OptionsCtrlImpl::Color::setLabel(const mu_text* szLabel)
+{
+ m_strLabel = szLabel;
+
+ // only if not showing button (otherwise update when button disappears)
+ if (!m_hColorWnd)
+ {
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+ }
+}
+
+COLORREF OptionsCtrlImpl::Color::getColor()
+{
+ if (m_hColorWnd)
+ {
+ m_crColor = getColorValue();
+ }
+
+ return m_crColor;
+}
+
+void OptionsCtrlImpl::Color::setColor(COLORREF crColor)
+{
+ m_crColor = crColor;
+
+ if (m_hColorWnd)
+ {
+ SendMessage(m_hColorWnd, CPM_SETCOLOUR, 0, crColor);
+ }
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_combo.cpp b/plugins/HistoryStats/src/optionsctrlimpl_combo.cpp
new file mode 100644
index 0000000000..a019bda875
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_combo.cpp
@@ -0,0 +1,226 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+#include "main.h"
+
+/*
+ * OptionsCtrlImpl::Combo
+ */
+
+void OptionsCtrlImpl::Combo::enableChildsCombo()
+{
+ if (m_bDisableChilds || m_bDisableChildsOnIndex0)
+ {
+ enableChilds(getChildEnable());
+ }
+}
+
+bool OptionsCtrlImpl::Combo::getChildEnable()
+{
+ return
+ !m_bDisableChildsOnIndex0 && m_bDisableChilds && m_bEnabled ||
+ m_bDisableChildsOnIndex0 && (m_nSelected != 0) && (!m_bDisableChilds || m_bEnabled);
+}
+
+int OptionsCtrlImpl::Combo::getComboSel()
+{
+ int nSel = SendMessage(m_hComboWnd, CB_GETCURSEL, 0, 0);
+
+ return (nSel == CB_ERR) ? -1 : nSel;
+}
+
+ext::string OptionsCtrlImpl::Combo::getCombinedText()
+{
+ if (m_nSelected == -1)
+ {
+ return m_strLabel;
+ }
+ else
+ {
+ ext::string strTemp = m_strLabel;
+
+ strTemp += muT(": ");
+ strTemp += m_Items[m_nSelected];
+
+ return strTemp;
+ }
+}
+
+OptionsCtrlImpl::Combo::Combo(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itCombo, szLabel, dwFlags, dwData), m_hComboWnd(NULL), m_nSelected(-1)
+{
+ m_bDisableChildsOnIndex0 = bool_(dwFlags & OCF_DISABLECHILDSONINDEX0);
+
+ m_pCtrl->insertItem(pParent, this, getCombinedText().c_str(), dwFlags, m_bEnabled ? siCombo : siComboG);
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::Combo::onSelect()
+{
+ if (!m_bEnabled || m_hComboWnd)
+ {
+ return;
+ }
+
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+
+ HFONT hTreeFront = reinterpret_cast<HFONT>(SendMessage(m_pCtrl->m_hTree, WM_GETFONT, 0, 0));
+ RECT r;
+
+ if (m_pCtrl->getItemFreeRect(m_hItem, r))
+ {
+ r.top -= 2;
+ r.bottom += 2;
+
+ if (r.left + 50 > r.right)
+ {
+ r.left = r.right - 50;
+ }
+
+ HWND hTempWnd;
+
+ DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST;
+
+ if (hTempWnd = CreateWindowEx(
+ WS_EX_CLIENTEDGE, WC_COMBOBOX, muT(""), dwStyle,
+ r.left, r.top, r.right - r.left, (r.bottom - r.top) * 20,
+ m_pCtrl->m_hTree, reinterpret_cast<HMENU>(ccCombo), g_hInst, NULL))
+ {
+ vector_each_(i, m_Items)
+ {
+ SendMessage(hTempWnd, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(m_Items[i].c_str()));
+ }
+
+ if (m_nSelected != -1)
+ {
+ SendMessage(hTempWnd, CB_SETCURSEL, m_nSelected, 0);
+ }
+
+ SendMessage(hTempWnd, WM_SETFONT, reinterpret_cast<WPARAM>(hTreeFront), MAKELPARAM(TRUE, 0));
+
+ m_hComboWnd = hTempWnd;
+ }
+ }
+}
+
+void OptionsCtrlImpl::Combo::onDeselect()
+{
+ if (m_hComboWnd)
+ {
+ RECT rToInvalidate;
+ bool bValidRect = false;
+
+ if (GetWindowRect(m_hComboWnd, &rToInvalidate))
+ {
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 0);
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 1);
+
+ bValidRect = true;
+ }
+
+ m_nSelected = getComboSel();
+
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+
+ DestroyWindow(m_hComboWnd);
+ m_hComboWnd = NULL;
+
+ InvalidateRect(m_pCtrl->m_hTree, bValidRect ? &rToInvalidate : NULL, TRUE);
+
+ // enable childs?
+ enableChildsCombo();
+ }
+}
+
+void OptionsCtrlImpl::Combo::onActivate()
+{
+ if (!m_hComboWnd)
+ {
+ onSelect();
+ }
+
+ if (m_hComboWnd)
+ {
+ SetFocus(m_hComboWnd);
+ }
+}
+
+void OptionsCtrlImpl::Combo::onSelChanged()
+{
+ if (m_hComboWnd)
+ {
+ m_nSelected = getComboSel();
+
+ // enable childs?
+ enableChildsCombo();
+ }
+}
+
+void OptionsCtrlImpl::Combo::setEnabled(bool bEnable)
+{
+ m_bEnabled = bEnable;
+
+ m_pCtrl->setStateImage(m_hItem, bEnable ? siCombo : siComboG);
+
+ enableChildsCombo();
+}
+
+void OptionsCtrlImpl::Combo::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds || m_bDisableChildsOnIndex0)
+ {
+ pChild->setEnabled(getChildEnable());
+ }
+}
+
+void OptionsCtrlImpl::Combo::setLabel(const mu_text* szLabel)
+{
+ m_strLabel = szLabel;
+
+ // only if not editing (otherwise update when user finishes editing)
+ if (!m_hComboWnd)
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+}
+
+void OptionsCtrlImpl::Combo::addItem(const mu_text* szItem)
+{
+ m_Items.push_back(szItem);
+}
+
+int OptionsCtrlImpl::Combo::getSelected()
+{
+ if (m_hComboWnd)
+ {
+ m_nSelected = getComboSel();
+ }
+
+ return m_nSelected;
+}
+
+void OptionsCtrlImpl::Combo::setSelected(int nSelect)
+{
+ if (nSelect < 0 || nSelect >= m_Items.size())
+ {
+ return;
+ }
+
+ m_nSelected = nSelect;
+
+ if (m_hComboWnd)
+ {
+ SendMessage(m_hComboWnd, CB_SETCURSEL, m_nSelected, 0);
+ }
+ else
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+
+ // enable childs?
+ enableChildsCombo();
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_datetime.cpp b/plugins/HistoryStats/src/optionsctrlimpl_datetime.cpp
new file mode 100644
index 0000000000..9c703630ee
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_datetime.cpp
@@ -0,0 +1,429 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+#include "main.h"
+
+/*
+ * OptionsCtrlImpl::DateTime
+ */
+
+ext::string OptionsCtrlImpl::DateTime::getDTFormatString(const ext::string& strFormat)
+{
+ ext::string strOut, strPart;
+
+ for (int i = 0; i < strFormat.length(); ++i)
+ {
+ if (strFormat[i] == muC('%') && i < strFormat.length() - 1)
+ {
+ ++i;
+
+ bool bSharp = (strFormat[i] == muC('#'));
+ ext::string strCode;
+
+ if (bSharp && i < strFormat.length() - 1)
+ {
+ ++i;
+ }
+
+ switch (strFormat[i])
+ {
+ case muC('a'):
+ strCode = muT("ddd");
+ break;
+
+ case muC('A'):
+ strCode = muT("dddd");
+ break;
+
+ case muC('b'):
+ strCode = muT("MMM");
+ break;
+
+ case muC('B'):
+ strCode = muT("MMMM");
+ break;
+
+ case muC('d'):
+ strCode = bSharp ? muT("d") : muT("dd");
+ break;
+
+ case muC('H'):
+ strCode = bSharp ? muT("H") : muT("HH");
+ break;
+
+ case muC('I'):
+ strCode = bSharp ? muT("h") : muT("hh");
+ break;
+
+ case muC('m'):
+ strCode = bSharp ? muT("M") : muT("MM");
+ break;
+
+ case muC('M'):
+ strCode = bSharp ? muT("m") : muT("mm");
+ break;
+
+ case muC('p'):
+ strCode = muT("tt"); // MEMO: seems not to work if current locale is 24-hour
+ break;
+
+ case muC('y'):
+ strCode = muT("yy");
+ break;
+
+ case muC('Y'):
+ strCode = muT("yyyy");
+ break;
+
+ case muC('%'):
+ strPart += muT("%");
+ break;
+ }
+
+ if (!strCode.empty())
+ {
+ if (!strPart.empty())
+ {
+ strOut += muT("'");
+ strOut += strPart;
+ strOut += muT("'");
+ strPart = muT("");
+ }
+
+ strOut += strCode;
+ }
+ }
+ else
+ {
+ strPart += strFormat[i];
+
+ if (strFormat[i] == muC('\''))
+ {
+ strPart += muT("'");
+ }
+ }
+ }
+
+ if (!strPart.empty())
+ {
+ strOut += muT("'");
+ strOut += strPart;
+ strOut += muT("'");
+ }
+
+ return strOut;
+}
+
+SYSTEMTIME OptionsCtrlImpl::DateTime::toSystemTime(DWORD dwTimestamp)
+{
+ SYSTEMTIME st;
+ FILETIME ft;
+ LONGLONG ll = Int32x32To64(dwTimestamp, 10000000) + 116444736000000000;
+
+ ft.dwLowDateTime = static_cast<DWORD>(ll);
+ ft.dwHighDateTime = static_cast<DWORD>(ll >> 32);
+
+ FileTimeToSystemTime(&ft, &st);
+
+ return st;
+}
+
+DWORD OptionsCtrlImpl::DateTime::fromSystemTime(const SYSTEMTIME& st)
+{
+ FILETIME ft;
+ LONGLONG ll;
+ DWORD dwTimestamp;
+
+ SystemTimeToFileTime(&st, &ft);
+
+ ll = (static_cast<LONGLONG>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
+
+ dwTimestamp = static_cast<DWORD>((ll - 116444736000000000) / 10000000);
+
+ return dwTimestamp;
+}
+
+void OptionsCtrlImpl::DateTime::enableChildsDateTime()
+{
+ if (m_bDisableChilds || m_bDisableChildsOnNone)
+ {
+ enableChilds(getChildEnable());
+ }
+}
+
+bool OptionsCtrlImpl::DateTime::getChildEnable()
+{
+ return
+ !m_bDisableChildsOnNone && m_bDisableChilds && m_bEnabled ||
+ m_bDisableChildsOnNone && !m_bNone && (!m_bDisableChilds || m_bEnabled);
+}
+
+DWORD OptionsCtrlImpl::DateTime::getTimestampValue()
+{
+ SYSTEMTIME st;
+
+ if (SendMessage(m_hDateTimeWnd, DTM_GETSYSTEMTIME, 0, reinterpret_cast<LPARAM>(&st)) == GDT_VALID)
+ {
+ return fromSystemTime(st);
+ }
+ else
+ {
+ return 24 * 60 * 60;
+ }
+}
+
+bool OptionsCtrlImpl::DateTime::getTimestampNone()
+{
+ if (!m_bAllowNone)
+ {
+ return false;
+ }
+
+ SYSTEMTIME st;
+
+ return (SendMessage(m_hDateTimeWnd, DTM_GETSYSTEMTIME, 0, reinterpret_cast<LPARAM>(&st)) != GDT_VALID);
+}
+
+ext::string OptionsCtrlImpl::DateTime::getCombinedText()
+{
+ ext::string strTemp = m_strLabel;
+
+ strTemp += muT(": ");
+
+ if (m_bNone)
+ {
+ strTemp += i18n(muT("none"));
+ }
+ else
+ {
+ strTemp += utils::timestampToString(m_dwTimestamp, m_strFormat.c_str());
+ }
+
+ return strTemp;
+}
+
+OptionsCtrlImpl::DateTime::DateTime(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, const mu_text* szFormat, DWORD dwTimestamp, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itDateTime, szLabel, dwFlags, dwData), m_hDateTimeWnd(NULL), m_strFormat(szFormat), m_dwTimestamp(dwTimestamp)
+{
+ m_bDisableChildsOnNone = bool_(dwFlags & OCF_DISABLECHILDSONNONE);
+ m_bAllowNone = bool_(dwFlags & OCF_ALLOWNONE);
+ m_bNone = m_bAllowNone && bool_(dwFlags & OCF_NONE);
+ m_strFormatDT = getDTFormatString(m_strFormat);
+
+ m_pCtrl->insertItem(pParent, this, getCombinedText().c_str(), dwFlags, m_bEnabled ? siDateTime : siDateTimeG);
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::DateTime::onSelect()
+{
+ if (!m_bEnabled || m_hDateTimeWnd)
+ {
+ return;
+ }
+
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+
+ HFONT hTreeFront = reinterpret_cast<HFONT>(SendMessage(m_pCtrl->m_hTree, WM_GETFONT, 0, 0));
+ RECT r;
+
+ if (m_pCtrl->getItemFreeRect(m_hItem, r))
+ {
+ r.top -= 2;
+ r.bottom += 2;
+
+ if (r.left + 50 > r.right)
+ {
+ r.left = r.right - 50;
+ }
+
+ HWND hTempWnd;
+
+ DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | (m_bAllowNone ? DTS_SHOWNONE : 0);
+
+ if (hTempWnd = CreateWindowEx(
+ WS_EX_CLIENTEDGE, DATETIMEPICK_CLASS, muT(""), dwStyle,
+ r.left, r.top, r.right - r.left, r.bottom - r.top,
+ m_pCtrl->m_hTree, reinterpret_cast<HMENU>(ccDateTime), g_hInst, NULL))
+ {
+ // restrict to dates a timestamp can hold (with 1 day less to avoid timezone issues)
+ SYSTEMTIME stMinMax[2] = { toSystemTime(0x00000000 + 24 * 60 * 60), toSystemTime(0x7FFFFFFF - 24 * 60 * 60) };
+
+ SendMessage(hTempWnd, DTM_SETRANGE, GDTR_MIN | GDTR_MAX, reinterpret_cast<LPARAM>(stMinMax));
+
+ // set format string
+ SendMessage(hTempWnd, DTM_SETFORMAT, 0, reinterpret_cast<LPARAM>(m_strFormatDT.c_str()));
+
+ // set timestamp
+ if (m_bAllowNone && m_bNone)
+ {
+ SendMessage(hTempWnd, DTM_SETSYSTEMTIME, GDT_NONE, 0);
+ }
+ else
+ {
+ SYSTEMTIME st = toSystemTime(m_dwTimestamp);
+
+ SendMessage(hTempWnd, DTM_SETSYSTEMTIME, GDT_VALID, reinterpret_cast<LPARAM>(&st));
+ }
+
+ SendMessage(hTempWnd, WM_SETFONT, reinterpret_cast<WPARAM>(hTreeFront), MAKELPARAM(TRUE, 0));
+
+ m_hDateTimeWnd = hTempWnd;
+ }
+ }
+}
+
+void OptionsCtrlImpl::DateTime::onDeselect()
+{
+ if (m_hDateTimeWnd)
+ {
+ RECT rToInvalidate;
+ bool bValidRect = false;
+
+ if (GetWindowRect(m_hDateTimeWnd, &rToInvalidate))
+ {
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 0);
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 1);
+
+ bValidRect = true;
+ }
+
+ m_dwTimestamp = getTimestampValue();
+ m_bNone = getTimestampNone();
+
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+
+ DestroyWindow(m_hDateTimeWnd);
+ m_hDateTimeWnd = NULL;
+
+ InvalidateRect(m_pCtrl->m_hTree, bValidRect ? &rToInvalidate : NULL, TRUE);
+
+ // enable childs?
+ enableChildsDateTime();
+ }
+}
+
+void OptionsCtrlImpl::DateTime::onActivate()
+{
+ if (!m_hDateTimeWnd)
+ {
+ onSelect();
+ }
+
+ if (m_hDateTimeWnd)
+ {
+ SetFocus(m_hDateTimeWnd);
+ }
+}
+
+void OptionsCtrlImpl::DateTime::onDateTimeChange()
+{
+ if (m_hDateTimeWnd)
+ {
+ m_dwTimestamp = getTimestampValue();
+ m_bNone = getTimestampNone();
+
+ // enable childs?
+ enableChildsDateTime();
+ }
+}
+
+void OptionsCtrlImpl::DateTime::setEnabled(bool bEnable)
+{
+ m_bEnabled = bEnable;
+
+ m_pCtrl->setStateImage(m_hItem, bEnable ? siDateTime : siDateTimeG);
+
+ enableChildsDateTime();
+}
+
+void OptionsCtrlImpl::DateTime::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds || m_bDisableChildsOnNone)
+ {
+ pChild->setEnabled(getChildEnable());
+ }
+}
+
+void OptionsCtrlImpl::DateTime::setLabel(const mu_text* szLabel)
+{
+ m_strLabel = szLabel;
+
+ // only if not editing (otherwise update when user finishes editing)
+ if (!m_hDateTimeWnd)
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+}
+
+bool OptionsCtrlImpl::DateTime::isNone()
+{
+ if (m_hDateTimeWnd)
+ {
+ m_dwTimestamp = getTimestampValue();
+ m_bNone = getTimestampNone();
+ }
+
+ return m_bNone;
+}
+
+void OptionsCtrlImpl::DateTime::setNone()
+{
+ if (!m_bAllowNone)
+ {
+ return;
+ }
+
+ m_bNone = true;
+
+ if (m_hDateTimeWnd)
+ {
+ SendMessage(m_hDateTimeWnd, DTM_SETSYSTEMTIME, GDT_NONE, 0);
+ }
+ else
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+
+ // enable childs?
+ enableChildsDateTime();
+}
+
+DWORD OptionsCtrlImpl::DateTime::getTimestamp()
+{
+ if (m_hDateTimeWnd)
+ {
+ m_dwTimestamp = getTimestampValue();
+ m_bNone = getTimestampNone();
+ }
+
+ return m_dwTimestamp;
+}
+
+void OptionsCtrlImpl::DateTime::setTimestamp(DWORD dwTimestamp)
+{
+ m_bNone = false;
+ m_dwTimestamp = dwTimestamp;
+
+ if (m_hDateTimeWnd)
+ {
+ SYSTEMTIME st = toSystemTime(dwTimestamp);
+
+ SendMessage(m_hDateTimeWnd, DTM_SETSYSTEMTIME, GDT_VALID, reinterpret_cast<LPARAM>(&st));
+ }
+ else
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+
+ // enable childs?
+ enableChildsDateTime();
+}
+
+bool OptionsCtrlImpl::DateTime::isMonthCalVisible()
+{
+ return (m_hDateTimeWnd && SendMessage(m_hDateTimeWnd, DTM_GETMONTHCAL, 0, 0));
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_edit.cpp b/plugins/HistoryStats/src/optionsctrlimpl_edit.cpp
new file mode 100644
index 0000000000..8af65d71dd
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_edit.cpp
@@ -0,0 +1,190 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+#include "main.h"
+
+/*
+ * OptionsCtrlImpl::Edit
+ */
+
+ext::string OptionsCtrlImpl::Edit::getEditText()
+{
+ int nLen = GetWindowTextLength(m_hEditWnd);
+ mu_text* szBuf = new mu_text[nLen + 1];
+
+ GetWindowText(m_hEditWnd, szBuf, nLen + 1);
+
+ ext::string strTemp = szBuf;
+
+ delete[] szBuf;
+
+ return strTemp;
+}
+
+ext::string OptionsCtrlImpl::Edit::getCombinedText()
+{
+ if (m_strEdit.empty())
+ {
+ return m_strLabel;
+ }
+ else
+ {
+ ext::string strTemp = m_strLabel;
+
+ strTemp += muT(": ");
+ strTemp += m_strEdit;
+
+ return strTemp;
+ }
+}
+
+OptionsCtrlImpl::Edit::Edit(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, const mu_text* szEdit, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itEdit, szLabel, dwFlags, dwData), m_hEditWnd(NULL)
+{
+ m_strEdit = szEdit;
+ m_bNumber = bool_(dwFlags & OCF_NUMBER);
+
+ m_pCtrl->insertItem(pParent, this, getCombinedText().c_str(), dwFlags, m_bEnabled ? siEdit : siEditG);
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::Edit::onSelect()
+{
+ if (!m_bEnabled || m_hEditWnd)
+ {
+ return;
+ }
+
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+
+ HFONT hTreeFront = reinterpret_cast<HFONT>(SendMessage(m_pCtrl->m_hTree, WM_GETFONT, 0, 0));
+ RECT r;
+
+ if (m_pCtrl->getItemFreeRect(m_hItem, r))
+ {
+ r.top -= 2;
+ r.bottom += 2;
+
+ if (r.left + 50 > r.right)
+ {
+ r.left = r.right - 50;
+ }
+
+ HWND hTempWnd;
+ DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_LEFT | ES_AUTOHSCROLL;
+
+ if (m_bNumber)
+ {
+ dwStyle |= ES_NUMBER | ES_RIGHT;
+ }
+
+ if (hTempWnd = CreateWindowEx(
+ WS_EX_CLIENTEDGE, WC_EDIT, m_strEdit.c_str(), dwStyle,
+ r.left, r.top, r.right - r.left, r.bottom - r.top,
+ m_pCtrl->m_hTree, reinterpret_cast<HMENU>(ccEdit), g_hInst, NULL))
+ {
+ SendMessage(hTempWnd, WM_SETFONT, reinterpret_cast<WPARAM>(hTreeFront), MAKELPARAM(TRUE, 0));
+
+ m_hEditWnd = hTempWnd;
+ }
+ }
+}
+
+void OptionsCtrlImpl::Edit::onDeselect()
+{
+ if (m_hEditWnd)
+ {
+ RECT rToInvalidate;
+ bool bValidRect = false;
+
+ if (GetWindowRect(m_hEditWnd, &rToInvalidate))
+ {
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 0);
+ ScreenToClient(m_pCtrl->m_hTree, reinterpret_cast<POINT*>(&rToInvalidate) + 1);
+
+ bValidRect = true;
+ }
+
+ m_strEdit = getEditText();
+
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+
+ DestroyWindow(m_hEditWnd);
+ m_hEditWnd = NULL;
+
+ InvalidateRect(m_pCtrl->m_hTree, bValidRect ? &rToInvalidate : NULL, TRUE);
+ }
+}
+
+void OptionsCtrlImpl::Edit::onActivate()
+{
+ if (!m_hEditWnd)
+ {
+ onSelect();
+ }
+
+ if (m_hEditWnd)
+ {
+ SetFocus(m_hEditWnd);
+ SendMessage(m_hEditWnd, EM_SETSEL, 0, -1);
+ }
+}
+
+void OptionsCtrlImpl::Edit::setEnabled(bool bEnable)
+{
+ m_bEnabled = bEnable;
+
+ m_pCtrl->setStateImage(m_hItem, bEnable ? siEdit : siEditG);
+
+ if (m_bDisableChilds)
+ {
+ enableChilds(m_bEnabled);
+ }
+}
+
+void OptionsCtrlImpl::Edit::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds)
+ {
+ pChild->setEnabled(m_bEnabled);
+ }
+}
+
+const mu_text* OptionsCtrlImpl::Edit::getString()
+{
+ if (m_hEditWnd)
+ {
+ m_strEdit = getEditText();
+ }
+
+ return m_strEdit.c_str();
+}
+
+void OptionsCtrlImpl::Edit::setString(const mu_text* szString)
+{
+ m_strEdit = szString;
+
+ if (m_hEditWnd)
+ {
+ SetWindowText(m_hEditWnd, m_strEdit.c_str());
+ }
+ else
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+}
+
+void OptionsCtrlImpl::Edit::setLabel(const mu_text* szLabel)
+{
+ m_strLabel = szLabel;
+
+ // only if not editing (otherwise update when user finishes editing)
+ if (!m_hEditWnd)
+ {
+ m_pCtrl->setNodeText(m_hItem, getCombinedText().c_str());
+ }
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_group.cpp b/plugins/HistoryStats/src/optionsctrlimpl_group.cpp
new file mode 100644
index 0000000000..0c389e517f
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_group.cpp
@@ -0,0 +1,39 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+/*
+ * OptionsCtrlImpl::Group
+ */
+
+OptionsCtrlImpl::Group::Group(OptionsCtrlImpl* pCtrl, Item* pParent, const mu_text* szLabel, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itGroup, szLabel, dwFlags, dwData)
+{
+ m_bDrawLine = bool_(dwFlags & OCF_DRAWLINE);
+
+ pCtrl->insertItem(pParent, this, szLabel, dwFlags, m_bEnabled ? siFolder : siFolderG);
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+void OptionsCtrlImpl::Group::setEnabled(bool bEnable)
+{
+ m_bEnabled = bEnable;
+
+ m_pCtrl->setStateImage(m_hItem, m_bEnabled ? siFolder : siFolderG);
+
+ if (m_bDisableChilds)
+ {
+ enableChilds(m_bEnabled);
+ }
+}
+
+void OptionsCtrlImpl::Group::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds)
+ {
+ pChild->setEnabled(m_bEnabled);
+ }
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_item.cpp b/plugins/HistoryStats/src/optionsctrlimpl_item.cpp
new file mode 100644
index 0000000000..e46a154629
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_item.cpp
@@ -0,0 +1,37 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+/*
+ * OptionsCtrlImpl::Item
+ */
+
+OptionsCtrlImpl::Item::Item(OptionsCtrlImpl* pCtrl, ItemType ItemType, const mu_text* szLabel, DWORD dwFlags, DWORD dwData)
+ : m_pCtrl(pCtrl), m_hItem(NULL), m_nRef(1), m_ItemType(ItemType), m_strLabel(szLabel), m_dwData(dwData)
+{
+ m_bEnabled = !(dwFlags & OCF_DISABLED);
+ m_bDisableChilds = !(dwFlags & OCF_NODISABLECHILDS);
+}
+
+void OptionsCtrlImpl::Item::enableChilds(bool bEnable)
+{
+ HTREEITEM hChild = TreeView_GetChild(m_pCtrl->m_hTree, m_hItem);
+
+ while (hChild)
+ {
+ Item* pItem = m_pCtrl->getItem(hChild);
+
+ if (pItem)
+ {
+ pItem->setEnabled(bEnable);
+ }
+
+ hChild = TreeView_GetNextSibling(m_pCtrl->m_hTree, hChild);
+ }
+}
+
+void OptionsCtrlImpl::Item::setLabel(const mu_text* szLabel)
+{
+ m_strLabel = szLabel;
+
+ m_pCtrl->setNodeText(m_hItem, m_strLabel.c_str());
+}
diff --git a/plugins/HistoryStats/src/optionsctrlimpl_radio.cpp b/plugins/HistoryStats/src/optionsctrlimpl_radio.cpp
new file mode 100644
index 0000000000..75491192af
--- /dev/null
+++ b/plugins/HistoryStats/src/optionsctrlimpl_radio.cpp
@@ -0,0 +1,125 @@
+#include "_globals.h"
+#include "optionsctrlimpl.h"
+
+/*
+ * OptionsCtrlImpl::RadioSiblings
+ */
+
+int OptionsCtrlImpl::RadioSiblings::join(Radio* pRadio)
+{
+ m_Radios.push_back(pRadio);
+ ++m_nRadios;
+
+ return m_nRadios - 1;
+}
+
+bool OptionsCtrlImpl::RadioSiblings::leave(int nRadio)
+{
+ assert(nRadio >= 0 && nRadio < m_Radios.size() && m_Radios[nRadio]);
+
+ m_Radios[nRadio] = NULL;
+ --m_nRadios;
+
+ return (m_nRadios == 0);
+}
+
+void OptionsCtrlImpl::RadioSiblings::setChecked(int nRadio)
+{
+ if (nRadio == m_nChecked)
+ {
+ return;
+ }
+
+ if (nRadio < 0 || nRadio >= m_Radios.size())
+ {
+ return;
+ }
+
+ if (m_nChecked != -1)
+ {
+ assert(m_Radios[m_nChecked]);
+
+ int nOldChecked = m_nChecked;
+
+ m_nChecked = -1;
+ m_Radios[nOldChecked]->updateItem();
+ }
+
+ assert(m_Radios[nRadio]);
+
+ m_nChecked = nRadio;
+ m_Radios[m_nChecked]->updateItem();
+}
+
+/*
+ * OptionsCtrlImpl::Radio
+ */
+
+bool OptionsCtrlImpl::Radio::getChildEnable(bool bChecked)
+{
+ return
+ !m_bDisableChildsOnUncheck && m_bDisableChilds && m_bEnabled ||
+ m_bDisableChildsOnUncheck && bChecked && (!m_bDisableChilds || m_bEnabled);
+}
+
+void OptionsCtrlImpl::Radio::updateItem()
+{
+ bool bChecked = isChecked();
+
+ m_pCtrl->setStateImage(m_hItem, m_bEnabled ? (bChecked ? siRadioC : siRadioU) : (bChecked ? siRadioCG : siRadioUG));
+
+ if (m_bDisableChilds || m_bDisableChildsOnUncheck)
+ {
+ enableChilds(getChildEnable(bChecked));
+ }
+}
+
+OptionsCtrlImpl::Radio::Radio(OptionsCtrlImpl* pCtrl, Item* pParent, Radio* pSibling, const mu_text* szLabel, DWORD dwFlags, DWORD dwData)
+ : Item(pCtrl, itRadio, szLabel, dwFlags, dwData)
+{
+ m_bDisableChildsOnUncheck = bool_(dwFlags & OCF_DISABLECHILDSONUNCHECK);
+
+ pCtrl->insertItem(pParent, this, szLabel, dwFlags, siRadioU);
+
+ m_pSiblings = pSibling ? pSibling->m_pSiblings : new RadioSiblings();
+ m_nIndex = m_pSiblings->join(this);
+
+ if (dwFlags & OCF_CHECKED)
+ {
+ m_pSiblings->setChecked(m_nIndex);
+ }
+ else
+ {
+ updateItem();
+ }
+
+ if (pParent)
+ {
+ pParent->childAdded(this);
+ }
+}
+
+OptionsCtrlImpl::Radio::~Radio()
+{
+ if (m_pSiblings->leave(m_nIndex))
+ {
+ delete m_pSiblings;
+ }
+}
+
+void OptionsCtrlImpl::Radio::onToggle()
+{
+ if (m_bEnabled && !isChecked())
+ {
+ setChecked();
+ m_pCtrl->setModified(this);
+ }
+}
+
+void OptionsCtrlImpl::Radio::childAdded(Item* pChild)
+{
+ if (m_bDisableChilds || m_bDisableChildsOnUncheck)
+ {
+ pChild->setEnabled(getChildEnable(isChecked()));
+ }
+}
diff --git a/plugins/HistoryStats/src/protocol.cpp b/plugins/HistoryStats/src/protocol.cpp
new file mode 100644
index 0000000000..e71340d800
--- /dev/null
+++ b/plugins/HistoryStats/src/protocol.cpp
@@ -0,0 +1,18 @@
+#include "_globals.h"
+#include "protocol.h"
+
+#include "utils.h"
+
+ext::string Protocol::getDisplayName(const ext::a::string& protocol)
+{
+ mu_text protoName[128];
+
+ if (mu::protosvc::getName(protocol.c_str(), 128, protoName) == 0)
+ {
+ return protoName;
+ }
+ else
+ {
+ return utils::fromA(protocol);
+ }
+}
diff --git a/plugins/HistoryStats/src/protocol.h b/plugins/HistoryStats/src/protocol.h
new file mode 100644
index 0000000000..6ea9a44a41
--- /dev/null
+++ b/plugins/HistoryStats/src/protocol.h
@@ -0,0 +1,18 @@
+#if !defined(HISTORYSTATS_GUARD_PROTOCOL_H)
+#define HISTORYSTATS_GUARD_PROTOCOL_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include <set>
+
+class Protocol
+{
+public:
+ static ext::string getDisplayName(const ext::a::string& protocol);
+
+public:
+ ext::string displayName;
+};
+
+#endif // HISTORYSTATS_GUARD_PROTOCOL_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/resource.h b/plugins/HistoryStats/src/resource.h
new file mode 100644
index 0000000000..d64b457a5f
--- /dev/null
+++ b/plugins/HistoryStats/src/resource.h
@@ -0,0 +1,86 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by resource.rc
+//
+#define IDD_PROGRESS 102
+#define IDI_HISTORYSTATS 102
+#define IDD_COLADD 116
+#define IDD_CONFLICT 117
+#define IDD_SUB_GLOBAL 119
+#define IDI_COL_ADD 123
+#define IDI_COL_UP 124
+#define IDI_COL_DEL 125
+#define IDI_COL_DOWN 126
+#define IDI_SETT_GLOBAL 127
+#define IDI_SETT_COLUMNS 128
+#define IDI_SETT_EXCLUDE 129
+#define IDI_SETT_INPUT 130
+#define IDI_SETT_OUTPUT 131
+#define IDI_CREATE 132
+#define IDD_SUB_EXCLUDE 133
+#define IDD_SUB_INPUT 134
+#define IDI_EXCLUDE_NO 135
+#define IDI_EXCLUDE_YES 136
+#define IDI_TREE_RADIO4 137
+#define IDI_TREE_CHECK1 138
+#define IDI_TREE_CHECK2 139
+#define IDI_TREE_CHECK3 140
+#define IDI_TREE_CHECK4 141
+#define IDI_TREE_EDIT1 142
+#define IDI_TREE_EDIT2 143
+#define IDI_TREE_FOLDER1 144
+#define IDI_TREE_FOLDER2 145
+#define IDI_TREE_RADIO1 146
+#define IDI_TREE_RADIO2 147
+#define IDI_TREE_RADIO3 148
+#define IDI_CREATE_WARNING 149
+#define IDI_DROPDOWN 150
+#define IDI_TREE_COMBO1 151
+#define IDI_TREE_COMBO2 152
+#define IDD_SUPPORTINFO 153
+#define IDD_FILTERWORDS 154
+#define IDI_TREE_BUTTON1 155
+#define IDI_TREE_BUTTON2 156
+#define IDD_CONFIGURE 157
+#define IDI_TREE_DATETIME1 158
+#define IDI_TREE_DATETIME2 159
+#define IDD_OPTIONS 236
+#define IDD_SUB_OUTPUT 237
+#define IDD_SUB_COLUMNS 238
+#define IDC_MAINBAR 1013
+#define IDC_MAINTEXT 1026
+#define IDC_SUBBAR 1038
+#define IDC_SUBTEXT 1039
+#define IDC_MAINPERCENT 1063
+#define IDC_SUBPERCENT 1064
+#define IDC_COLUMNS 1065
+#define IDC_DESCRIPTION 1071
+#define IDC_COLUMN 1072
+#define IDC_FILES 1073
+#define IDC_BAND 1083
+#define IDC_OPTIONS 1089
+#define IDC_CONTACTS 1090
+#define IDC_INFO 1091
+#define IDC_INFOLABEL 1093
+#define IDC_PLUGIN 1098
+#define IDC_FEATURES 1099
+#define IDC_LINK1 1101
+#define IDC_LINK2 1102
+#define IDC_SETS 1103
+#define IDC_MODE 1104
+#define IDC_WORDS 1105
+#define IDC_SETNAME 1106
+#define IDC_CUSTOM1 1112
+#define IDC_APPLY 1113
+#define IDC_MESSAGE 1114
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 160
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1115
+#define _APS_NEXT_SYMED_VALUE 102
+#endif
+#endif
diff --git a/plugins/HistoryStats/src/settings.cpp b/plugins/HistoryStats/src/settings.cpp
new file mode 100644
index 0000000000..67d395c332
--- /dev/null
+++ b/plugins/HistoryStats/src/settings.cpp
@@ -0,0 +1,646 @@
+#include "_globals.h"
+#include "settings.h"
+
+#include "utils.h"
+#include "main.h"
+#include "column.h"
+#include "dlgfilterwords.h"
+#include "colbase_words.h"
+
+/*
+ * Settings::CharMapper
+ */
+
+Settings::CharMapper::CharMapper(const Settings& settings)
+{
+ static const mu_text* defaultWordDelimiters = muT("\n\r\t");
+
+ array_each_(i, m_CharMap)
+ {
+ m_CharMap[i] = static_cast<mu_text>(i);
+ }
+
+ LCID lcid = GetUserDefaultLCID();
+ int len = LCMapString(
+ lcid, LCMAP_LINGUISTIC_CASING | LCMAP_LOWERCASE,
+ m_CharMap + 1, array_len(m_CharMap) - 1,
+ m_CharMap + 1, array_len(m_CharMap) - 1);
+
+ assert(len == array_len(m_CharMap) - 1);
+
+ upto_each_(i, 3)
+ {
+ m_CharMap[static_cast<t_uchar>(defaultWordDelimiters[i])] = muC(' ');
+ }
+
+ upto_each_(i, settings.m_WordDelimiters.length())
+ {
+ m_CharMap[static_cast<t_uchar>(settings.m_WordDelimiters[i])] = muC(' ');
+ }
+}
+
+/*
+ * Settings::Filter
+ */
+
+Settings::Filter::Filter(const ext::string& strID)
+ : m_strID(strID), m_nMode(fwmWordsMatching), m_nRef(0)
+{
+ time_t curTime = time(NULL);
+ struct tm curTM = *localtime(&curTime);
+
+ m_strName += utils::intToPadded(1900 + curTM.tm_year, 4);
+ m_strName += muT("-");
+ m_strName += utils::intToPadded(1 + curTM.tm_mon, 2);
+ m_strName += muT("-");
+ m_strName += utils::intToPadded(curTM.tm_mday, 2);
+ m_strName += muT(" ");
+ m_strName += utils::intToPadded(curTM.tm_hour, 2);
+ m_strName += muT(":");
+ m_strName += utils::intToPadded(curTM.tm_min, 2);
+ m_strName += muT(":");
+ m_strName += utils::intToPadded(curTM.tm_sec, 2);
+ m_strName += muT(".");
+ m_strName += utils::intToPadded(GetTickCount() % 1000, 3);
+}
+
+/*
+ * Settings
+ */
+
+const mu_text* Settings::getDefaultWordDelimiters()
+{
+ return muT(".?!,:;()[]{}<>+-*/=\\_^&\"'~%#@|$");
+}
+
+const mu_text* Settings::getDefaultStyleSheet()
+{
+ static ext::string StyleSheet;
+
+ if (StyleSheet.empty())
+ {
+ // general formats
+ StyleSheet +=
+ muT("h1 { font-family: Verdana, Arial, sans-serif; font-size: 16pt; }\n")
+ muT("div, span, td { font-family: Verdana, Arial, sans-serif; font-size: 10pt; }\n")
+ muT("span[title], td[title], div[title] { cursor: help; }\n")
+ muT("span[title] { border-bottom: 1px dotted; }\n")
+ muT("span[title]:hover, td[title]:hover { background-color: #FFFFCF; }\n")
+ muT("table { border-collapse: collapse; }\n")
+ muT("td { border: 1px solid ") + utils::colorToHTML(con::ColorBorder) + muT("; text-align: left; padding: 2px 4px 2px 4px; }\n")
+ muT("div.footer { padding: 12px 0px 8px 0px; }\n");
+
+ // special row formats
+ StyleSheet +=
+ muT("tr.header td { background-color: ") + utils::colorToHTML(con::ColorHeader) + muT("; text-align: center; }\n")
+ muT("tr.header td div { height: 1px; overflow: hidden; }\n")
+ muT("tr.omitted td { background-color: ") + utils::colorToHTML(con::ColorOmitted) + muT("; }\n")
+ muT("tr.totals td { background-color: ") + utils::colorToHTML(con::ColorTotals) + muT("; }\n");
+
+ // special cell formats
+ StyleSheet +=
+ muT("td.num { text-align: right; }\n")
+ muT("td.bars_bottom { vertical-align: bottom; padding: 4px 0px 0px 0px; }\n")
+ muT("td.bars_middle { vertical-align: middle; padding: 2px 0px 2px 0px; }\n")
+ muT("td.img_bottom { vertical-align: bottom; text-align: center; padding: 4px 0px 0px 0px; }\n")
+ muT("td.img_middle { vertical-align: middle; text-align: center; padding: 2px 0px 2px 0px; }\n");
+ }
+
+ return StyleSheet.c_str();
+}
+
+const mu_text* Settings::getDefaultHideContactMenuProtos()
+{
+ return muT("{num:0;}");
+}
+
+const mu_text* Settings::getDefaultProtosIgnore()
+{
+ return muT("{num:0;}");
+}
+
+const mu_text* Settings::getDefaultColumns()
+{
+ return
+ muT("{num:11;}")
+ muT("0{enabled:y;guid:rank;}")
+ muT("1{enabled:y;guid:nick;}1/data{detail:y;}")
+ muT("2{enabled:y;guid:protocol;}")
+ muT("3{enabled:y;guid:group;}")
+ muT("4{enabled:y;guid:inout;}4/data{abs_time:1;absolute:n;detail:y;detail_percent:n;graph_percent:y;show_sum:y;source:0;}")
+ muT("5{enabled:y;guid:inout;}5/data{abs_time:1;absolute:n;detail:y;detail_percent:n;graph_percent:y;show_sum:y;source:1;}")
+ muT("6{enabled:y;guid:inout;}6/data{abs_time:1;absolute:n;detail:y;detail_percent:n;graph_percent:y;show_sum:y;source:2;}")
+ muT("7{enabled:y;guid:chatduration;}7/data{detail:y;graph:y;vis_mode:3;}")
+ muT("8{enabled:y;guid:commonwords;}8/data{detail:y;filter_links:y;filter_words:0;in_out_color:n;max_length:0;min_length:1;num:10;offset:0;source:2;vis_mode:0;}")
+ muT("9{enabled:y;guid:wordcount;}9/data{detail:y;filter_links:y;filter_words:0;max_length:0;min_length:1;source:2;vis_mode:0;}")
+ muT("10{enabled:y;guid:split;}10/data{block_unit:0;blocks:28;detail:y;graph_align:1;source:0;source_type:2;units_per_block:6;vis_mode:0;}");
+}
+
+const mu_text* Settings::getDefaultSort()
+{
+ return
+ muT("0{by:17;asc:n}")
+ muT("1{by:-1;asc:y}")
+ muT("2{by:-1;asc:y}");
+}
+
+const mu_text* Settings::getDefaultOutputFile()
+{
+ return muT("HistoryStats\\stats.html");
+}
+
+const mu_text* Settings::getDefaultOutputExtraFolder()
+{
+ return muT("extra");
+}
+
+const mu_text* Settings::getDefaultFilterWords()
+{
+ return muT("{num:0;}");
+}
+
+void Settings::clearColumns()
+{
+ while (countCol() > 0)
+ {
+ delCol(0);
+ }
+}
+
+Settings::Settings()
+ : m_VersionCurrent(g_pluginInfoEx.version)
+ // global settings
+ , m_OnStartup(false)
+ , m_ShowMainMenu(true)
+ , m_ShowMainMenuSub(true)
+ , m_ShowContactMenu(true)
+ , m_ShowContactMenuPseudo(false)
+// , m_HideContactMenuProtos
+ , m_GraphicsMode(gmHTML)
+ , m_PNGMode(pmHTMLFallBack)
+ , m_ThreadLowPriority(true)
+ , m_PathToBrowser(muT(""))
+ // input settings
+ , m_ChatSessionMinDur(0)
+ , m_ChatSessionTimeout(900)
+ , m_AverageMinTime(0)
+ , m_WordDelimiters(muT("")) // see below
+// , m_ProtosIgnore
+ , m_IgnoreOld(0)
+ , m_IgnoreBefore(muT(""))
+ , m_IgnoreAfter(muT(""))
+ , m_FilterRawRTF(false)
+ , m_FilterBBCodes(false)
+ , m_MetaContactsMode(mcmBoth)
+ , m_MergeContacts(false)
+ , m_MergeContactsGroups(false)
+ , m_MergeMode(mmStrictMerge)
+ // output settings
+ , m_RemoveEmptyContacts(false)
+ , m_RemoveOutChatsZero(false)
+ , m_RemoveOutBytesZero(false)
+ , m_RemoveInChatsZero(false)
+ , m_RemoveInBytesZero(false)
+ , m_OmitContacts(false)
+ , m_OmitByValue(false)
+ , m_OmitByValueData(obvChatsTotal)
+ , m_OmitByValueLimit(5)
+ , m_OmitByTime(false)
+ , m_OmitByTimeDays(180)
+ , m_OmitByRank(true)
+ , m_OmitNumOnTop(10)
+ , m_OmittedInTotals(true)
+ , m_OmittedInExtraRow(true)
+ , m_CalcTotals(true)
+ , m_TableHeader(true)
+ , m_TableHeaderRepeat(0)
+ , m_TableHeaderVerbose(false)
+ , m_HeaderTooltips(true)
+ , m_HeaderTooltipsIfCustom(true)
+// , m_Sort
+ , m_OwnNick(muT("")) // see below
+ , m_OutputVariables(false)
+ , m_OutputFile(muT("")) // see below
+ , m_OutputExtraToFolder(true)
+ , m_OutputExtraFolder(muT("")) // see below
+ , m_OverwriteAlways(false)
+ , m_AutoOpenOptions(false)
+ , m_AutoOpenStartup(false)
+ , m_AutoOpenMenu(false)
+ // shared column data
+// , m_FilterWords
+{
+ m_WordDelimiters = getDefaultWordDelimiters();
+ m_OwnNick = i18n(muT("(default nick)"));
+ m_OutputFile = getDefaultOutputFile();
+ m_OutputExtraFolder = getDefaultOutputExtraFolder();
+
+ m_Sort[0].by = skBytesTotalAvg; m_Sort[0].asc = false;
+ m_Sort[1].by = skNothing; m_Sort[1].asc = true;
+ m_Sort[2].by = skNothing; m_Sort[2].asc = true;
+}
+
+Settings::Settings(const Settings& other)
+ : m_VersionCurrent(g_pluginInfoEx.version)
+{
+ *this = other;
+}
+
+Settings::~Settings()
+{
+ clearColumns();
+}
+
+int Settings::addCol(Column* pCol)
+{
+ m_Columns.push_back(pCol);
+
+ return m_Columns.size() - 1;
+}
+
+bool Settings::delCol(int index)
+{
+ delete m_Columns[index];
+ m_Columns.erase(m_Columns.begin() + index);
+
+ return true;
+}
+
+bool Settings::delCol(Column* pCol)
+{
+ vector_each_(i, m_Columns)
+ {
+ if (m_Columns[i] == pCol)
+ {
+ return delCol(i);
+ }
+ }
+
+ return false;
+}
+
+bool Settings::moveCol(Column* pCol, Column* pInsertAfter)
+{
+ assert(pCol);
+
+ if (pCol == pInsertAfter)
+ {
+ return true;
+ }
+
+ int nColIndex = -1;
+
+ vector_each_(i, m_Columns)
+ {
+ if (m_Columns[i] == pCol)
+ {
+ nColIndex = i;
+ break;
+ }
+ }
+
+ if (nColIndex == -1)
+ {
+ return false;
+ }
+
+ int nInsertIndex = -1;
+
+ if (pInsertAfter)
+ {
+ vector_each_(i, m_Columns)
+ {
+ if (m_Columns[i] == pInsertAfter)
+ {
+ nInsertIndex = i;
+ break;
+ }
+ }
+
+ if (nInsertIndex == -1)
+ {
+ return false;
+ }
+ }
+
+ if (nInsertIndex == nColIndex - 1)
+ {
+ return true;
+ }
+
+ if (nInsertIndex < nColIndex)
+ {
+ ++nInsertIndex;
+ }
+
+ m_Columns.erase(m_Columns.begin() + nColIndex);
+ m_Columns.insert(m_Columns.begin() + nInsertIndex, pCol);
+
+ return true;
+}
+
+bool Settings::manageFilterWords(HWND hParent, Column* pCol)
+{
+ ColBaseWords* pWordsCol = reinterpret_cast<ColBaseWords*>(pCol);
+ DlgFilterWords dlg;
+
+ // update reference count in filters
+ ColFilterSet ReferencedFilters;
+
+ upto_each_(i, countCol())
+ {
+ Column* pCurCol = getCol(i);
+
+ if (pCurCol != pCol && pCurCol->getFeatures() & Column::cfIsColBaseWords)
+ {
+ ColBaseWords* pCurWordsCol = reinterpret_cast<ColBaseWords*>(pCurCol);
+
+ citer_each_(ColFilterSet, j, pCurWordsCol->getFilterWords())
+ {
+ ReferencedFilters.insert(*j);
+ }
+ }
+ }
+
+ iter_each_(FilterSet, i, m_FilterWords)
+ {
+ Filter *F = (Filter*)i.operator->();
+ if (ReferencedFilters.find(i->getID()) != ReferencedFilters.end())
+ F->setRef(1);
+ else
+ F->setRef(0);
+ }
+
+ // init dialog
+ dlg.setFilters(m_FilterWords);
+ dlg.setColFilters(pWordsCol->getFilterWords());
+
+ if (dlg.showModal(hParent))
+ {
+ // read data from dialog
+ dlg.updateFilters(m_FilterWords);
+ pWordsCol->setFilterWords(dlg.getColFilters());
+
+ // update other columns (in case of removed filters)
+ ColFilterSet ValidSets;
+
+ iter_each_(FilterSet, i, m_FilterWords)
+ {
+ ValidSets.insert(i->getID());
+ }
+
+ upto_each_(i, countCol())
+ {
+ Column* pCurCol = getCol(i);
+
+ if (pCurCol != pCol && pCurCol->getFeatures() & Column::cfIsColBaseWords)
+ {
+ ColBaseWords* pCurWordsCol = reinterpret_cast<ColBaseWords*>(pCurCol);
+ ReferencedFilters.clear();
+
+ citer_each_(ColFilterSet, j, pCurWordsCol->getFilterWords())
+ {
+ if (ValidSets.find(*j) != ValidSets.end())
+ {
+ ReferencedFilters.insert(*j);
+ }
+ }
+
+ pCurWordsCol->setFilterWords(ReferencedFilters);
+ }
+ }
+
+ // return true - means something was changed
+ return true;
+ }
+
+ // return false - means nothing was changed
+ return false;
+}
+
+const Settings::Filter* Settings::getFilter(const ext::string& strID) const
+{
+ citer_each_(FilterSet, i, m_FilterWords)
+ {
+ if (i->getID() == strID)
+ {
+ return &(*i);
+ }
+ }
+
+ return NULL;
+}
+
+Settings& Settings::operator =(const Settings& other)
+{
+ clearColumns();
+
+ // global settings
+ m_OnStartup = other.m_OnStartup;
+ m_ShowMainMenu = other.m_ShowMainMenu;
+ m_ShowMainMenuSub = other.m_ShowMainMenuSub;
+ m_ShowContactMenu = other.m_ShowContactMenu;
+ m_ShowContactMenuPseudo = other.m_ShowContactMenuPseudo;
+ m_HideContactMenuProtos = other.m_HideContactMenuProtos;
+ m_GraphicsMode = other.m_GraphicsMode;
+ m_PNGMode = other.m_PNGMode;
+ m_ThreadLowPriority = other.m_ThreadLowPriority;
+ m_PathToBrowser = other.m_PathToBrowser;
+
+ // input settings
+ m_ChatSessionMinDur = other.m_ChatSessionMinDur;
+ m_ChatSessionTimeout = other.m_ChatSessionTimeout;
+ m_AverageMinTime = other.m_AverageMinTime;
+ m_WordDelimiters = other.m_WordDelimiters;
+ m_ProtosIgnore = other.m_ProtosIgnore;
+ m_IgnoreOld = other.m_IgnoreOld;
+ m_IgnoreBefore = other.m_IgnoreBefore;
+ m_IgnoreAfter = other.m_IgnoreAfter;
+ m_FilterRawRTF = other.m_FilterRawRTF;
+ m_FilterBBCodes = other.m_FilterBBCodes;
+ m_MetaContactsMode = other.m_MetaContactsMode;
+ m_MergeContacts = other.m_MergeContacts;
+ m_MergeContactsGroups = other.m_MergeContactsGroups;
+ m_MergeMode = other.m_MergeMode;
+
+ // column settings
+ upto_each_(i, other.countCol())
+ {
+ addCol(other.getCol(i)->clone());
+ }
+
+ // output settings
+ m_RemoveEmptyContacts = other.m_RemoveEmptyContacts;
+ m_RemoveOutChatsZero = other.m_RemoveOutChatsZero;
+ m_RemoveOutBytesZero = other.m_RemoveOutBytesZero;
+ m_RemoveInChatsZero = other.m_RemoveInChatsZero;
+ m_RemoveInBytesZero = other.m_RemoveInBytesZero;
+ m_OmitContacts = other.m_OmitContacts;
+ m_OmitByValue = other.m_OmitByValue;
+ m_OmitByValueData = other.m_OmitByValueData;
+ m_OmitByValueLimit = other.m_OmitByValueLimit;
+ m_OmitByTime = other.m_OmitByTime;
+ m_OmitByTimeDays = other.m_OmitByTimeDays;
+ m_OmitByRank = other.m_OmitByRank;
+ m_OmitNumOnTop = other.m_OmitNumOnTop;
+ m_OmittedInTotals = other.m_OmittedInTotals;
+ m_OmittedInExtraRow = other.m_OmittedInExtraRow;
+ m_CalcTotals = other.m_CalcTotals;
+ m_TableHeader = other.m_TableHeader;
+ m_TableHeaderRepeat = other.m_TableHeaderRepeat;
+ m_TableHeaderVerbose = other.m_TableHeaderVerbose;
+ m_HeaderTooltips = other.m_HeaderTooltips;
+ m_HeaderTooltipsIfCustom = other.m_HeaderTooltipsIfCustom;
+
+ upto_each_(j, cNumSortLevels)
+ {
+ m_Sort[j] = other.m_Sort[j];
+ }
+
+ m_OwnNick = other.m_OwnNick;
+ m_OutputVariables = other.m_OutputVariables;
+ m_OutputFile = other.m_OutputFile;
+ m_OutputExtraToFolder = other.m_OutputExtraToFolder;
+ m_OutputExtraFolder = other.m_OutputExtraFolder;
+ m_OverwriteAlways = other.m_OverwriteAlways;
+ m_AutoOpenOptions = other.m_AutoOpenOptions;
+ m_AutoOpenStartup = other.m_AutoOpenStartup;
+ m_AutoOpenMenu = other.m_AutoOpenMenu;
+
+ // shared column data
+ m_FilterWords = other.m_FilterWords;
+
+ return *this;
+}
+
+ext::string Settings::getOutputFile(DWORD timeStarted) const
+{
+ ext::string strFile = m_OutputFile;
+
+ // perform variables substitution (if activated)
+ if (m_OutputVariables)
+ {
+ strFile = utils::replaceVariables(strFile, timeStarted, m_OwnNick.c_str());
+ }
+
+ if (utils::isRelative(strFile))
+ {
+ return utils::getMirandaPath() + strFile;
+ }
+ else
+ {
+ return strFile;
+ }
+}
+
+ext::string Settings::getOutputPrefix(DWORD timeStarted) const
+{
+ if (m_OutputExtraToFolder && !m_OutputExtraFolder.empty())
+ {
+ ext::string extraFolder = m_OutputExtraFolder;
+
+ // perform variables substitution (if activated)
+ if (m_OutputVariables)
+ {
+ extraFolder = utils::replaceVariables(extraFolder, timeStarted, m_OwnNick.c_str());
+ }
+
+ // strip leading backslashes
+ while (!extraFolder.empty() && extraFolder[0] == muC('\\'))
+ {
+ extraFolder.erase(0, 1);
+ }
+
+ // strip trailing backslashes
+ while (!extraFolder.empty() && extraFolder[extraFolder.length() - 1] == muC('\\'))
+ {
+ extraFolder.erase(extraFolder.length() - 1, 1);
+ }
+
+ // append, if still not empty
+ if (!extraFolder.empty())
+ {
+ return extraFolder + muT("\\");
+ }
+ }
+
+ return muT("");
+}
+
+bool Settings::isPNGOutputActiveAndAvailable() const
+{
+ return (m_GraphicsMode == gmPNG && Canvas::hasPNG());
+}
+
+DWORD Settings::getIgnoreBefore() const
+{
+ return utils::parseDate(m_IgnoreBefore);
+}
+
+DWORD Settings::getIgnoreAfter() const
+{
+ return utils::parseDate(m_IgnoreAfter);
+}
+
+
+void Settings::ensureConstraints()
+{
+ if (m_GraphicsMode < gmHTML || m_GraphicsMode > gmPNG)
+ {
+ m_GraphicsMode = gmHTML;
+ }
+
+ if (m_PNGMode < pmHTMLFallBack || m_PNGMode > pmPreferHTML)
+ {
+ m_PNGMode = pmHTMLFallBack;
+ }
+
+ if (m_IgnoreOld < 0)
+ {
+ m_IgnoreOld = 0;
+ }
+
+ if (m_ChatSessionMinDur < 0)
+ {
+ m_ChatSessionMinDur = 0;
+ }
+
+ if (m_ChatSessionTimeout < 1)
+ {
+ m_ChatSessionTimeout = 1;
+ }
+
+ utils::ensureRange(m_AverageMinTime, 0, 1000, 0);
+
+ if (m_MetaContactsMode < mcmMetaOnly || m_MetaContactsMode > mcmIgnoreMeta)
+ {
+ m_MetaContactsMode = mcmBoth;
+ }
+
+ if (m_MergeMode < mmTolerantMerge || m_MergeMode > mmNoMerge)
+ {
+ m_MergeMode = mmStrictMerge;
+ }
+
+ if (m_OmitByValueData < obvFIRST || m_OmitByValueData > obvLAST)
+ {
+ m_OmitByValueData = obvChatsTotal;
+ }
+
+ utils::ensureRange(m_OmitByValueLimit, 1, 10000000, 5);
+ utils::ensureRange(m_OmitByTimeDays, 1, 10000, 180);
+ utils::ensureRange(m_OmitNumOnTop, 1, 10000, 10);
+ utils::ensureRange(m_TableHeaderRepeat, 0, 1000, 0);
+}
+
+void Settings::openURL(const mu_text* szURL)
+{
+ if (m_PathToBrowser.empty())
+ {
+ ShellExecute(NULL, muT("open"), szURL, NULL, NULL, SW_SHOWNORMAL);
+ }
+ else
+ {
+ ShellExecute(NULL, muT("open"), m_PathToBrowser.c_str(), szURL, NULL, SW_SHOWNORMAL);
+ }
+}
diff --git a/plugins/HistoryStats/src/settings.h b/plugins/HistoryStats/src/settings.h
new file mode 100644
index 0000000000..9af8f1f30a
--- /dev/null
+++ b/plugins/HistoryStats/src/settings.h
@@ -0,0 +1,311 @@
+#if !defined(HISTORYSTATS_GUARD_SETTINGS_H)
+#define HISTORYSTATS_GUARD_SETTINGS_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include "canvas.h"
+#include "mirandasettings.h"
+
+// #include "column.h"
+class Column;
+
+#include <set>
+#include <map>
+#include <vector>
+
+class Settings
+ : private pattern::NotCopyable<Settings>
+{
+public:
+ // "poor man's constants"
+ enum Constants {
+ cNumSortLevels = 3,
+ };
+
+ // graphic output modes
+ enum GraphicsMode {
+ gmHTML = 0,
+ gmPNG = 1,
+ };
+
+ // PNG output modes
+ enum PNGMode {
+ pmHTMLFallBack = 0,
+ pmEnforcePNG = 1,
+ pmPreferHTML = 2,
+ };
+
+ // meta contact handling modes
+ enum MetaContactsMode {
+ mcmMetaOnly = 0,
+ mcmSubOnly = 1,
+ mcmBoth = 2,
+ mcmIgnoreMeta = 3,
+ };
+
+ // event merge mode
+ enum MergeMode {
+ mmTolerantMerge = 0,
+ mmStrictMerge = 1,
+ mmNoMerge = 2,
+ };
+
+ // sort keys
+ enum SortKeyEnum {
+ skFIRST = 0,
+ skLAST = 26,
+ skNothing = -1,
+ skNick = 0,
+ skProtocol = 1,
+ skGroup = 2,
+ skBytesOut = 3,
+ skBytesIn = 4,
+ skBytesTotal = 5,
+ skMessagesOut = 6,
+ skMessagesIn = 7,
+ skMessagesTotal = 8,
+ skChatsOut = 9,
+ skChatsIn = 10,
+ skChatsTotal = 11,
+ skChatDurationTotal = 12,
+ skTimeOfFirstMessage = 13,
+ skTimeOfLastMessage = 14,
+ skBytesOutAvg = 15,
+ skBytesInAvg = 16,
+ skBytesTotalAvg = 17,
+ skMessagesOutAvg = 18,
+ skMessagesInAvg = 19,
+ skMessagesTotalAvg = 20,
+ skChatsOutAvg = 21,
+ skChatsInAvg = 22,
+ skChatsTotalAvg = 23,
+ skChatDurationMin = 24,
+ skChatDurationAvg = 25,
+ skChatDurationMax = 26,
+ };
+
+ // omit by value data
+ enum OmitByValueEnum {
+ obvFIRST = 0,
+ obvLAST = 18,
+ obvBytesIn = 0,
+ obvBytesOut = 1,
+ obvBytesTotal = 2,
+ obvBytesInAvg = 3,
+ obvBytesOutAvg = 4,
+ obvBytesTotalAvg = 5,
+ obvMessagesIn = 6,
+ obvMessagesOut = 7,
+ obvMessagesTotal = 8,
+ obvMessagesInAvg = 9,
+ obvMessagesOutAvg = 10,
+ obvMessafesTotalAvg = 11,
+ obvChatsIn = 12,
+ obvChatsOut = 13,
+ obvChatsTotal = 14,
+ obvChatsInAvg = 15,
+ obvChatsOutAvg = 16,
+ obvChatsTotalAvg = 17,
+ obvChatDurationTotal = 18,
+ };
+
+ // button IDs for columns
+ enum ButtonID {
+ biFilterWords = 100,
+ };
+
+ // filter word modes
+ enum FilterWordsMode {
+ fwmFIRST = 0,
+ fwmLAST = 7,
+ fwmWordsMatching = 0,
+ fwmWordsContaining = 1,
+ fwmWordsStartingWith = 2,
+ fwmWordsEndingWith = 3,
+ fwmMessagesMatching = 4,
+ fwmMessagesContaining = 5,
+ fwmMessagesStartingWith = 6,
+ fwmMessagesEndingWith = 7,
+ };
+
+ class CharMapper
+ : private pattern::NotCopyable<Settings>
+ {
+ private:
+#if defined(MU_WIDE)
+ typedef unsigned short t_uchar;
+#else // MU_WIDE
+ typedef unsigned char t_uchar;
+#endif // MU_WIDE
+#define T_NUMCHARS (1 << (sizeof(mu_text) * 8))
+
+ private:
+ mu_text m_CharMap[T_NUMCHARS];
+
+#undef T_NUMCHARS
+
+ public:
+ explicit CharMapper(const Settings& settings);
+ mu_text mapChar(mu_text c) const { return m_CharMap[static_cast<t_uchar>(c)]; }
+ };
+
+ typedef std::set<ext::string> WordSet;
+
+ class Filter
+ {
+ private:
+ ext::string m_strID;
+ ext::string m_strName;
+ int m_nMode;
+ WordSet m_Words;
+ int m_nRef;
+
+ public:
+ explicit Filter(const ext::string& strID);
+
+ bool operator ==(const Filter& other) const { return m_strID == other.m_strID; }
+ bool operator <(const Filter& other) const { return m_strID < other.m_strID; }
+
+ const ext::string& getID() const { return m_strID; }
+ const ext::string& getName() const { return m_strName; }
+ void setName(const ext::string& strName) { m_strName = strName; }
+ int getMode() const { return m_nMode; }
+ void setMode(int nMode) { m_nMode = nMode; }
+ const WordSet& getWords() const { return m_Words; }
+ void setWords(const WordSet& Words) { m_Words = Words; }
+ void clearWords() { m_Words.clear(); }
+ void addWord(const ext::string& strWord) { m_Words.insert(strWord); }
+ int getRef() const { return m_nRef; }
+ void setRef(int nRef) { m_nRef = nRef; }
+ };
+
+ typedef std::set<Filter> FilterSet;
+ typedef std::set<ext::string> ColFilterSet;
+
+ struct SortKey {
+ int by;
+ bool asc;
+ };
+
+public:
+ typedef std::set<ext::a::string> ProtoSet;
+ typedef std::vector<Column*> ColumnList;
+
+public:
+ static const mu_text* getDefaultWordDelimiters();
+ static const mu_text* getDefaultStyleSheet();
+ static const mu_text* getDefaultHideContactMenuProtos();
+ static const mu_text* getDefaultProtosIgnore();
+ static const mu_text* getDefaultColumns();
+ static const mu_text* getDefaultSort();
+ static const mu_text* getDefaultOutputFile();
+ static const mu_text* getDefaultOutputExtraFolder();
+ static const mu_text* getDefaultFilterWords();
+
+public:
+ const DWORD m_VersionCurrent;
+
+private:
+ // column settings
+ ColumnList m_Columns;
+
+protected:
+ void clearColumns();
+
+public:
+ // global settings
+ bool m_OnStartup;
+ bool m_ShowMainMenu;
+ bool m_ShowMainMenuSub;
+ bool m_ShowContactMenu;
+ bool m_ShowContactMenuPseudo;
+ ProtoSet m_HideContactMenuProtos;
+ int m_GraphicsMode;
+ int m_PNGMode;
+ bool m_ThreadLowPriority;
+ ext::string m_PathToBrowser;
+
+ // input settings
+ int m_ChatSessionMinDur;
+ int m_ChatSessionTimeout;
+ int m_AverageMinTime;
+ ext::string m_WordDelimiters;
+ ProtoSet m_ProtosIgnore;
+ int m_IgnoreOld;
+ ext::string m_IgnoreBefore;
+ ext::string m_IgnoreAfter;
+ bool m_FilterRawRTF;
+ bool m_FilterBBCodes;
+ int m_MetaContactsMode;
+ bool m_MergeContacts;
+ bool m_MergeContactsGroups;
+ int m_MergeMode;
+
+ // output settings
+ bool m_RemoveEmptyContacts;
+ bool m_RemoveOutChatsZero;
+ bool m_RemoveOutBytesZero;
+ bool m_RemoveInChatsZero;
+ bool m_RemoveInBytesZero;
+ bool m_OmitContacts;
+ bool m_OmitByValue;
+ int m_OmitByValueData;
+ int m_OmitByValueLimit;
+ bool m_OmitByTime;
+ int m_OmitByTimeDays;
+ bool m_OmitByRank;
+ int m_OmitNumOnTop;
+ bool m_OmittedInTotals;
+ bool m_OmittedInExtraRow;
+ bool m_CalcTotals;
+ bool m_TableHeader;
+ int m_TableHeaderRepeat;
+ bool m_TableHeaderVerbose;
+ bool m_HeaderTooltips;
+ bool m_HeaderTooltipsIfCustom;
+ SortKey m_Sort[cNumSortLevels];
+ ext::string m_OwnNick;
+ bool m_OutputVariables;
+ ext::string m_OutputFile;
+ bool m_OutputExtraToFolder;
+ ext::string m_OutputExtraFolder;
+ bool m_OverwriteAlways;
+ bool m_AutoOpenOptions;
+ bool m_AutoOpenStartup;
+ bool m_AutoOpenMenu;
+
+ // shared column data
+ FilterSet m_FilterWords;
+
+public:
+ explicit Settings();
+ explicit Settings(const Settings& other);
+ Settings& operator =(const Settings& other);
+ ~Settings();
+
+ void assignSettings(const Settings& other) { operator =(other); }
+
+ int countCol() const { return m_Columns.size(); }
+ const Column* getCol(int index) const { return m_Columns[index]; }
+ Column* getCol(int index) { return m_Columns[index]; }
+ int addCol(Column* pCol);
+ bool delCol(int index);
+ bool delCol(Column* pCol);
+ bool moveCol(Column* pCol, Column* pInsertAfter);
+
+ bool manageFilterWords(HWND hParent, Column* pCol);
+ const Filter* getFilter(const ext::string& strID) const;
+
+ ext::string getOutputFile(DWORD timeStarted) const;
+ ext::string getOutputPrefix(DWORD timeStarted) const;
+ bool isPNGOutputActiveAndAvailable() const;
+ DWORD getIgnoreBefore() const;
+ DWORD getIgnoreAfter() const;
+
+ void ensureConstraints();
+
+ void openURL(const mu_text* szURL);
+};
+
+#endif // HISTORYSTATS_GUARD_SETTINGS_H
diff --git a/plugins/HistoryStats/src/settingsserializer.cpp b/plugins/HistoryStats/src/settingsserializer.cpp
new file mode 100644
index 0000000000..669ddeb346
--- /dev/null
+++ b/plugins/HistoryStats/src/settingsserializer.cpp
@@ -0,0 +1,512 @@
+#include "_globals.h"
+#include "settingsserializer.h"
+
+#include "settingstree.h"
+#include "column.h"
+
+/*
+ * SettingsSerializer
+ */
+
+static const mu_ansi* g_UsedSettings[] = {
+ // special
+ con::SettVersion,
+ con::SettLastPage,
+ con::SettShowColumnInfo,
+ con::SettShowSupportInfo,
+ con::SettLastStatisticsFile,
+ // normal
+ con::SettAutoOpenOptions,
+ con::SettAutoOpenStartup,
+ con::SettAutoOpenMenu,
+ con::SettAverageMinTime,
+ con::SettCalcTotals,
+ con::SettChatSessionMinDur,
+ con::SettChatSessionTimeout,
+ con::SettColumns,
+ con::SettFilterBBCodes,
+ con::SettFilterWords,
+ con::SettGraphicsMode,
+ con::SettHeaderTooltips,
+ con::SettHeaderTooltipsIfCustom,
+ con::SettHideContactMenuProtos,
+ con::SettIgnoreAfter,
+ con::SettIgnoreBefore,
+ con::SettIgnoreOld,
+ con::SettMenuItem,
+ con::SettMergeContacts,
+ con::SettMergeContactsGroups,
+ con::SettMergeMode,
+ con::SettMetaContactsMode,
+ con::SettNickname,
+ con::SettOmitByRank,
+ con::SettOmitByTime,
+ con::SettOmitByTimeDays,
+ con::SettOmitByValue,
+ con::SettOmitByValueData,
+ con::SettOmitByValueLimit,
+ con::SettOmitContacts,
+ con::SettOmitNumOnTop,
+ con::SettOmittedInTotals,
+ con::SettOmittedInExtraRow,
+ con::SettOnStartup,
+ con::SettOutput,
+ con::SettOutputExtraFolder,
+ con::SettOutputExtraToFolder,
+ con::SettOutputVariables,
+ con::SettOverwriteAlways,
+ con::SettPathToBrowser,
+ con::SettPNGMode,
+ con::SettProtosIgnore,
+ con::SettRemoveEmptyContacts,
+ con::SettRemoveInChatsZero,
+ con::SettRemoveInBytesZero,
+ con::SettRemoveOutChatsZero,
+ con::SettRemoveOutBytesZero,
+ con::SettShowContactMenu,
+ con::SettShowContactMenuPseudo,
+ con::SettShowMenuSub,
+ con::SettSort,
+ con::SettTableHeader,
+ con::SettTableHeaderRepeat,
+ con::SettTableHeaderVerbose,
+ con::SettThreadLowPriority,
+ con::SettWordDelimiters,
+};
+
+SettingsSerializer::SettingsSerializer(const mu_ansi* module)
+{
+ m_DB.setContact(0);
+ m_DB.setModule(module);
+}
+
+void SettingsSerializer::readFromDB()
+{
+ clearColumns();
+
+ ext::string defaultNick = mu::clist::getContactDisplayName(0);
+ SettingsTree settingsTree;
+
+ // read version tag
+ m_VersionInDB = m_DB.readDWord(con::SettVersion, 0);
+
+ // -- global settings --
+
+ m_OnStartup = m_DB.readBool(con::SettOnStartup , false);
+ m_GraphicsMode = m_DB.readByte(con::SettGraphicsMode , gmHTML);
+ m_PNGMode = m_DB.readByte(con::SettPNGMode , pmHTMLFallBack);
+ m_ShowMainMenu = m_DB.readBool(con::SettMenuItem , true);
+ m_ShowMainMenuSub = m_DB.readBool(con::SettShowMenuSub , true);
+ m_ShowContactMenu = m_DB.readBool(con::SettShowContactMenu , true);
+ m_ShowContactMenuPseudo = m_DB.readBool(con::SettShowContactMenuPseudo, false);
+ m_ThreadLowPriority = m_DB.readBool(con::SettThreadLowPriority , true);
+ m_PathToBrowser = m_DB.readStr (con::SettPathToBrowser , muT(""));
+
+ m_HideContactMenuProtos.clear();
+ m_DB.readTree(con::SettHideContactMenuProtos, getDefaultHideContactMenuProtos(), settingsTree);
+
+ int nHideContactMenuProtos = settingsTree.readInt(con::KeyNum, 0);
+
+ upto_each_(i, nHideContactMenuProtos)
+ {
+ m_HideContactMenuProtos.insert(utils::toA(settingsTree.readStr(utils::intToString(i).c_str(), muT(""))));
+ }
+
+ // -- input settings --
+
+
+ m_ChatSessionMinDur = m_DB.readWord(con::SettChatSessionMinDur , 0);
+ m_ChatSessionTimeout = m_DB.readWord(con::SettChatSessionTimeout, 900);
+ m_AverageMinTime = m_DB.readWord(con::SettAverageMinTime , 0);
+ m_WordDelimiters = m_DB.readStr (con::SettWordDelimiters , getDefaultWordDelimiters());
+
+ m_ProtosIgnore.clear();
+ m_DB.readTree(con::SettProtosIgnore, getDefaultProtosIgnore(), settingsTree);
+
+ int nIgnoreProtos = settingsTree.readInt(con::KeyNum, 0);
+
+ upto_each_(i, nIgnoreProtos)
+ {
+ m_ProtosIgnore.insert(utils::toA(settingsTree.readStr(utils::intToString(i).c_str(), muT(""))));
+ }
+
+ m_IgnoreOld = m_DB.readWord(con::SettIgnoreOld , 0);
+ m_IgnoreBefore = m_DB.readStr (con::SettIgnoreBefore , muT(""));
+ m_IgnoreAfter = m_DB.readStr (con::SettIgnoreAfter , muT(""));
+ m_FilterRawRTF = m_DB.readBool(con::SettFilterRawRTF , false);
+ m_FilterBBCodes = m_DB.readBool(con::SettFilterBBCodes , false);
+ m_MetaContactsMode = m_DB.readByte(con::SettMetaContactsMode , mcmBoth);
+ m_MergeContacts = m_DB.readBool(con::SettMergeContacts , false);
+ m_MergeContactsGroups = m_DB.readBool(con::SettMergeContactsGroups, false);
+ m_MergeMode = m_DB.readByte(con::SettMergeMode , mmStrictMerge);
+
+ // -- column settings --
+
+ clearColumns();
+ m_DB.readTree(con::SettColumns, getDefaultColumns(), settingsTree);
+
+ int nCols = settingsTree.readInt(con::KeyNum, 0);
+
+ upto_each_(i, nCols)
+ {
+ ext::string colPrefix = utils::intToString(i);
+
+ settingsTree.setKey(colPrefix.c_str());
+
+ Column* pCol = Column::fromUID(settingsTree.readStr(con::KeyGUID, muT("")));
+
+ if (pCol)
+ {
+ pCol->setEnabled(settingsTree.readBool(con::KeyEnabled, true));
+ pCol->setCustomTitle(settingsTree.readStr(con::KeyTitle, muT("")));
+
+ settingsTree.setKey((colPrefix + con::SuffixData).c_str());
+
+ pCol->configRead(settingsTree);
+
+ addCol(pCol);
+ }
+ }
+
+ // -- output settings --
+
+ m_RemoveEmptyContacts = m_DB.readBool (con::SettRemoveEmptyContacts , false);
+ m_RemoveOutChatsZero = m_DB.readBool (con::SettRemoveOutChatsZero , false);
+ m_RemoveOutBytesZero = m_DB.readBool (con::SettRemoveOutBytesZero , false);
+ m_RemoveInChatsZero = m_DB.readBool (con::SettRemoveInChatsZero , false);
+ m_RemoveInBytesZero = m_DB.readBool (con::SettRemoveInBytesZero , false);
+ m_OmitContacts = m_DB.readBool (con::SettOmitContacts , false);
+ m_OmitByValue = m_DB.readBool (con::SettOmitByValue , false);
+ m_OmitByValueData = m_DB.readByte (con::SettOmitByValueData , obvChatsTotal);
+ m_OmitByValueLimit = m_DB.readDWord(con::SettOmitByValueLimit , 5);
+ m_OmitByTime = m_DB.readBool (con::SettOmitByTime , false);
+ m_OmitByTimeDays = m_DB.readWord (con::SettOmitByTimeDays , 180);
+ m_OmitByRank = m_DB.readBool (con::SettOmitByRank , true);
+ m_OmitNumOnTop = m_DB.readWord (con::SettOmitNumOnTop , 10);
+ m_OmittedInTotals = m_DB.readBool (con::SettOmittedInTotals , true);
+ m_OmittedInExtraRow = m_DB.readBool (con::SettOmittedInExtraRow , true);
+ m_CalcTotals = m_DB.readBool (con::SettCalcTotals , true);
+ m_TableHeader = m_DB.readBool (con::SettTableHeader , true);
+ m_TableHeaderRepeat = m_DB.readWord (con::SettTableHeaderRepeat , 0);
+ m_TableHeaderVerbose = m_DB.readBool (con::SettTableHeaderVerbose , false);
+ m_HeaderTooltips = m_DB.readBool (con::SettHeaderTooltips , true);
+ m_HeaderTooltipsIfCustom = m_DB.readBool (con::SettHeaderTooltipsIfCustom, true);
+
+ m_DB.readTree(con::SettSort, getDefaultSort(), settingsTree);
+
+ upto_each_(i, cNumSortLevels)
+ {
+ settingsTree.setKey(utils::intToString(i).c_str());
+
+ m_Sort[i].by = settingsTree.readIntRanged(con::KeyBy, (i == 0) ? skBytesTotalAvg : skNothing, (i == 0) ? skFIRST : skNothing, skLAST);
+ m_Sort[i].asc = settingsTree.readBool(con::KeyAsc, i != 0);
+ }
+
+ m_OwnNick = m_DB.readStr (con::SettNickname , defaultNick.c_str());
+ m_OutputVariables = m_DB.readBool(con::SettOutputVariables , false);
+ m_OutputFile = m_DB.readStr (con::SettOutput , getDefaultOutputFile());
+ m_OutputExtraToFolder = m_DB.readBool(con::SettOutputExtraToFolder, true);
+ m_OutputExtraFolder = m_DB.readStr (con::SettOutputExtraFolder , getDefaultOutputExtraFolder());
+ m_OverwriteAlways = m_DB.readBool(con::SettOverwriteAlways , false);
+ m_AutoOpenOptions = m_DB.readBool(con::SettAutoOpenOptions , false);
+ m_AutoOpenStartup = m_DB.readBool(con::SettAutoOpenStartup , false);
+ m_AutoOpenMenu = m_DB.readBool(con::SettAutoOpenMenu , false);
+
+ // -- shared column data --
+
+ m_FilterWords.clear();
+ m_DB.readTree(con::SettFilterWords, getDefaultFilterWords(), settingsTree);
+
+ int nFilters = settingsTree.readInt(con::KeyNum, 0);
+
+ upto_each_(i, nFilters)
+ {
+ ext::string strPrefix = utils::intToString(i);
+
+ // read filter attributes
+ settingsTree.setKey(strPrefix.c_str());
+
+ Filter* curFilter = (Filter*)&(m_FilterWords.insert(Filter(settingsTree.readStr(con::KeyID, muT("")))).first);
+
+ curFilter->setName(settingsTree.readStr(con::KeyName, muT("")));
+ curFilter->setMode(settingsTree.readIntRanged(con::KeyMode, fwmWordsMatching, fwmFIRST, fwmLAST));
+
+ int nNumWords = settingsTree.readInt(con::KeyNumWords, 0);
+ if (nNumWords > 0)
+ {
+ // read filter words
+ strPrefix += con::SuffixWords;
+ settingsTree.setKey(strPrefix.c_str());
+
+ upto_each_(j, nNumWords)
+ {
+ curFilter->addWord(settingsTree.readStr(utils::intToString(j).c_str(), muT("")));
+ }
+ }
+ }
+
+ m_DB.writeTree(con::SettFilterWords, settingsTree);
+
+ // ensure constraints
+ ensureConstraints();
+}
+
+void SettingsSerializer::writeToDB()
+{
+ // update silently if DB entries are from an older version
+ if (isDBUpdateNeeded())
+ {
+ updateDB();
+ }
+
+
+ SettingsTree settingsTree;
+
+ // -- global settings --
+
+ m_DB.writeBool(con::SettOnStartup , m_OnStartup);
+ m_DB.writeBool(con::SettMenuItem , m_ShowMainMenu);
+ m_DB.writeBool(con::SettShowMenuSub , m_ShowMainMenuSub);
+ m_DB.writeBool(con::SettShowContactMenu , m_ShowContactMenu);
+ m_DB.writeBool(con::SettShowContactMenuPseudo, m_ShowContactMenuPseudo);
+ m_DB.writeByte(con::SettGraphicsMode , m_GraphicsMode);
+ m_DB.writeByte(con::SettPNGMode , m_PNGMode);
+ m_DB.writeBool(con::SettThreadLowPriority , m_ThreadLowPriority);
+ m_DB.writeStr (con::SettPathToBrowser , m_PathToBrowser.c_str());
+
+ settingsTree.clear();
+ settingsTree.writeInt(con::KeyNum, m_HideContactMenuProtos.size());
+
+ {
+ int i = 0;
+
+ citer_each_(ProtoSet, j, m_HideContactMenuProtos)
+ {
+ settingsTree.writeStr(utils::intToString(i++).c_str(), utils::fromA(*j).c_str());
+ }
+ }
+
+ m_DB.writeTree(con::SettHideContactMenuProtos, settingsTree);
+
+ // -- input settings --
+
+ m_DB.writeWord(con::SettChatSessionMinDur , m_ChatSessionMinDur);
+ m_DB.writeWord(con::SettChatSessionTimeout, m_ChatSessionTimeout);
+ m_DB.writeWord(con::SettAverageMinTime , m_AverageMinTime);
+ m_DB.writeStr (con::SettWordDelimiters , m_WordDelimiters.c_str());
+
+ settingsTree.clear();
+ settingsTree.writeInt(con::KeyNum, m_ProtosIgnore.size());
+
+ {
+ int i = 0;
+
+ citer_each_(ProtoSet, j, m_ProtosIgnore)
+ {
+ settingsTree.writeStr(utils::intToString(i++).c_str(), utils::fromA(*j).c_str());
+ }
+ }
+
+ m_DB.writeTree(con::SettProtosIgnore, settingsTree);
+
+ m_DB.writeWord(con::SettIgnoreOld , m_IgnoreOld);
+ m_DB.writeStr (con::SettIgnoreBefore , m_IgnoreBefore.c_str());
+ m_DB.writeStr (con::SettIgnoreAfter , m_IgnoreAfter.c_str());
+ m_DB.writeBool(con::SettFilterRawRTF , m_FilterRawRTF);
+ m_DB.writeBool(con::SettFilterBBCodes , m_FilterBBCodes);
+ m_DB.writeByte(con::SettMetaContactsMode , m_MetaContactsMode);
+ m_DB.writeBool(con::SettMergeContacts , m_MergeContacts);
+ m_DB.writeBool(con::SettMergeContactsGroups, m_MergeContactsGroups);
+ m_DB.writeByte(con::SettMergeMode , m_MergeMode);
+
+ // -- column settings --
+
+ settingsTree.clear();
+ settingsTree.writeInt(con::KeyNum, countCol());
+
+ upto_each_(i, countCol())
+ {
+ ext::string colPrefix = utils::intToString(i);
+ const Column* pCol = getCol(i);
+
+ // write common data
+ settingsTree.setKey(colPrefix.c_str());
+
+ settingsTree.writeStr(con::KeyGUID, pCol->getUID());
+ settingsTree.writeBool(con::KeyEnabled, pCol->isEnabled());
+ settingsTree.writeStr(con::KeyTitle, pCol->getCustomTitle().c_str());
+
+ // write column specific data
+ colPrefix += con::SuffixData;
+ settingsTree.setKey(colPrefix.c_str());
+
+ pCol->configWrite(settingsTree);
+ }
+
+ m_DB.writeTree(con::SettColumns, settingsTree);
+
+ // -- output settings --
+
+ m_DB.writeBool (con::SettRemoveEmptyContacts , m_RemoveEmptyContacts);
+ m_DB.writeBool (con::SettRemoveOutChatsZero , m_RemoveOutChatsZero);
+ m_DB.writeBool (con::SettRemoveOutBytesZero , m_RemoveOutBytesZero);
+ m_DB.writeBool (con::SettRemoveInChatsZero , m_RemoveInChatsZero);
+ m_DB.writeBool (con::SettRemoveInBytesZero , m_RemoveInBytesZero);
+ m_DB.writeBool (con::SettOmitContacts , m_OmitContacts);
+ m_DB.writeBool (con::SettOmitByValue , m_OmitByValue);
+ m_DB.writeByte (con::SettOmitByValueData , m_OmitByValueData);
+ m_DB.writeDWord(con::SettOmitByValueLimit , m_OmitByValueLimit);
+ m_DB.writeBool (con::SettOmitByTime , m_OmitByTime);
+ m_DB.writeWord (con::SettOmitByTimeDays , m_OmitByTimeDays);
+ m_DB.writeBool (con::SettOmitByRank , m_OmitByRank);
+ m_DB.writeWord (con::SettOmitNumOnTop , m_OmitNumOnTop);
+ m_DB.writeBool (con::SettOmittedInTotals , m_OmittedInTotals);
+ m_DB.writeBool (con::SettOmittedInExtraRow , m_OmittedInExtraRow);
+ m_DB.writeBool (con::SettCalcTotals , m_CalcTotals);
+ m_DB.writeBool (con::SettTableHeader , m_TableHeader);
+ m_DB.writeWord (con::SettTableHeaderRepeat , m_TableHeaderRepeat);
+ m_DB.writeBool (con::SettTableHeaderVerbose , m_TableHeaderVerbose);
+ m_DB.writeBool (con::SettHeaderTooltips , m_HeaderTooltips);
+ m_DB.writeBool (con::SettHeaderTooltipsIfCustom, m_HeaderTooltipsIfCustom);
+
+ settingsTree.clear();
+
+ upto_each_(i, cNumSortLevels)
+ {
+ settingsTree.setKey(utils::intToString(i).c_str());
+
+ settingsTree.writeInt(con::KeyBy, m_Sort[i].by);
+ settingsTree.writeBool(con::KeyAsc, m_Sort[i].asc);
+ }
+
+ m_DB.writeStr (con::SettNickname , m_OwnNick.c_str());
+ m_DB.writeBool(con::SettOutputVariables , m_OutputVariables);
+ m_DB.writeStr (con::SettOutput , m_OutputFile.c_str());
+ m_DB.writeBool(con::SettOutputExtraToFolder, m_OutputExtraToFolder);
+ m_DB.writeStr (con::SettOutputExtraFolder , m_OutputExtraFolder.c_str());
+ m_DB.writeBool(con::SettOverwriteAlways , m_OverwriteAlways);
+ m_DB.writeBool(con::SettAutoOpenOptions , m_AutoOpenOptions);
+ m_DB.writeBool(con::SettAutoOpenStartup , m_AutoOpenStartup);
+ m_DB.writeBool(con::SettAutoOpenMenu , m_AutoOpenMenu);
+
+ m_DB.writeTree(con::SettSort, settingsTree);
+
+ // -- shared column data --
+
+ settingsTree.clear();
+ settingsTree.writeInt(con::KeyNum, m_FilterWords.size());
+
+ {
+ int nFilterNr = 0;
+
+ citer_each_(FilterSet, i, m_FilterWords)
+ {
+ ext::string strPrefix = utils::intToString(nFilterNr++);
+
+ // write filter attributes
+ settingsTree.setKey(strPrefix.c_str());
+ settingsTree.writeStr(con::KeyID, i->getID().c_str());
+ settingsTree.writeStr(con::KeyName, i->getName().c_str());
+ settingsTree.writeInt(con::KeyMode, i->getMode());
+ settingsTree.writeInt(con::KeyNumWords, i->getWords().size());
+
+ if (!i->getWords().empty())
+ {
+ // write filter words
+ strPrefix += con::SuffixWords;
+ settingsTree.setKey(strPrefix.c_str());
+
+ int nWordNr = 0;
+
+ citer_each_(WordSet, j, i->getWords())
+ {
+ settingsTree.writeStr(utils::intToString(nWordNr++).c_str(), j->c_str());
+ }
+ }
+ }
+ }
+
+ m_DB.writeTree(con::SettFilterWords, settingsTree);
+}
+
+bool SettingsSerializer::isDBUpdateNeeded()
+{
+ return (m_VersionInDB < m_VersionCurrent);
+}
+
+void SettingsSerializer::updateDB()
+{
+ std::set<ext::a::string> settings;
+ m_DB.enumSettings(std::inserter(settings, settings.begin()));
+
+ array_each_(i, g_UsedSettings)
+ {
+ settings.erase(g_UsedSettings[i]);
+ }
+
+ iter_each_(std::set<ext::a::string>, si, settings)
+ {
+ m_DB.delSetting((*si).c_str());
+ }
+
+ // write version tag
+ m_DB.writeDWord(con::SettVersion, m_VersionCurrent);
+ m_VersionInDB = m_VersionCurrent;
+}
+
+int SettingsSerializer::getLastPage()
+{
+ return m_DB.readDWord(con::SettLastPage, 0);
+}
+
+void SettingsSerializer::setLastPage(int nPage)
+{
+ m_DB.writeDWord(con::SettLastPage, nPage);
+}
+
+bool SettingsSerializer::getShowColumnInfo()
+{
+ return m_DB.readBool(con::SettShowColumnInfo, true);
+}
+
+void SettingsSerializer::setShowColumnInfo(bool bShow)
+{
+ m_DB.writeBool(con::SettShowColumnInfo, bShow);
+}
+
+bool SettingsSerializer::getShowSupportInfo()
+{
+ return m_DB.readBool(con::SettShowSupportInfo, false);
+}
+
+void SettingsSerializer::setShowSupportInfo(bool bShow)
+{
+ m_DB.writeBool(con::SettShowSupportInfo, bShow);
+}
+
+ext::string SettingsSerializer::getLastStatisticsFile()
+{
+ return m_DB.readStr(con::SettLastStatisticsFile, muT(""));
+}
+
+void SettingsSerializer::setLastStatisticsFile(const mu_text* szFileName)
+{
+ m_DB.writeStr(con::SettLastStatisticsFile, szFileName);
+}
+
+bool SettingsSerializer::canShowStatistics()
+{
+ ext::string strFileName = getLastStatisticsFile();
+
+ return !strFileName.empty() && utils::fileExists(strFileName);
+}
+
+void SettingsSerializer::showStatistics()
+{
+ ext::string strFileName = getLastStatisticsFile();
+
+ if (!strFileName.empty() && utils::fileExists(strFileName))
+ {
+ openURL(strFileName.c_str());
+ }
+}
diff --git a/plugins/HistoryStats/src/settingsserializer.h b/plugins/HistoryStats/src/settingsserializer.h
new file mode 100644
index 0000000000..1840757f54
--- /dev/null
+++ b/plugins/HistoryStats/src/settingsserializer.h
@@ -0,0 +1,41 @@
+#if !defined(HISTORYSTATS_GUARD_SETTINGSSERIALIZER_H)
+#define HISTORYSTATS_GUARD_SETTINGSSERIALIZER_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include "settings.h"
+
+class SettingsSerializer
+ : public Settings
+ , private pattern::NotCopyable<SettingsSerializer>
+{
+private:
+ DWORD m_VersionInDB;
+ MirandaSettings m_DB;
+
+public:
+ explicit SettingsSerializer(const mu_ansi* module);
+
+ void readFromDB();
+ void writeToDB();
+
+ bool isDBUpdateNeeded();
+ void updateDB();
+
+ int getLastPage();
+ void setLastPage(int nPage);
+
+ bool getShowColumnInfo();
+ void setShowColumnInfo(bool bShow);
+
+ bool getShowSupportInfo();
+ void setShowSupportInfo(bool bShow);
+
+ ext::string getLastStatisticsFile();
+ void setLastStatisticsFile(const mu_text* szFileName);
+ bool canShowStatistics();
+ void showStatistics();
+};
+
+#endif // HISTORYSTATS_GUARD_SETTINGSSERIALIZER_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/settingstree.cpp b/plugins/HistoryStats/src/settingstree.cpp
new file mode 100644
index 0000000000..6d4159d96f
--- /dev/null
+++ b/plugins/HistoryStats/src/settingstree.cpp
@@ -0,0 +1,260 @@
+#include "_globals.h"
+#include "settingstree.h"
+
+#include "utils.h"
+
+void SettingsTree::makeKeyValid()
+{
+ if (!m_pCurValues)
+ m_pCurValues = &m_Keys[m_CurKey];
+}
+
+SettingsTree::SettingsTree()
+ : m_pCurValues(NULL)
+{
+}
+
+SettingsTree::SettingsTree(const ext::string& config)
+ : m_pCurValues(NULL)
+{
+ fromString(config);
+}
+
+void SettingsTree::clear()
+{
+ m_Keys.clear();
+ setKey(muT(""));
+}
+
+void SettingsTree::fromString(const ext::string& config)
+{
+ m_Keys.clear();
+
+ ext::string::size_type i = 0;
+ ext::string curKey;
+
+ while (i < config.length())
+ {
+ if (config[i] == muC('{'))
+ {
+ ++i;
+
+ ValueMap& vals = m_Keys[curKey];
+
+ ext::string curSetting;
+ ext::string curVal;
+
+ while (i < config.length() && config[i] != muC('}'))
+ {
+ if (config[i] == muC(':'))
+ {
+ curSetting = curVal;
+ curVal = muT("");
+ }
+ else if (config[i] == muC(';'))
+ {
+ vals[curSetting] = curVal;
+
+ curSetting = muT("");
+ curVal = muT("");
+ }
+ else if (config[i] == muC('\\'))
+ {
+ ++i;
+ curVal += config[i];
+ }
+ else
+ {
+ curVal += config[i];
+ }
+
+ ++i;
+ }
+
+ curKey = muT("");
+ }
+ else
+ {
+ curKey += config[i];
+ }
+
+ ++i;
+ }
+
+ setKey(muT(""));
+}
+
+ext::string SettingsTree::toString() const
+{
+ static const mu_text* replaces[5][2] = {
+ { muT("\\"), muT("\\\\") },
+ { muT("{") , muT("\\{") },
+ { muT("}") , muT("\\}") },
+ { muT(":") , muT("\\:") },
+ { muT(";") , muT("\\;") }
+ };
+
+ ext::string data;
+
+ citer_each_(KeyMap, i, m_Keys)
+ {
+ const ValueMap& vals = i->second;
+
+ data += i->first;
+ data += muT("{");
+
+ citer_each_(ValueMap, j, vals)
+ {
+ data += j->first;
+ data += muT(":");
+
+ ext::string tempSecond = j->second;
+
+ array_each_(k, replaces)
+ {
+ utils::replaceAllInPlace(tempSecond, replaces[k][0], replaces[k][1]);
+ }
+
+ data += tempSecond;
+ data += muT(";");
+ }
+
+ data += muT("}");
+ }
+
+ return data;
+}
+
+void SettingsTree::setKey(const mu_text* key)
+{
+ m_CurKey = key;
+
+ KeyMap::iterator i = m_Keys.find(key);
+
+ if (i != m_Keys.end())
+ {
+ m_pCurValues = &i->second;
+ }
+ else
+ {
+ m_pCurValues = NULL;
+ }
+}
+
+bool SettingsTree::readBool(const mu_text* setting, bool errorValue) const
+{
+ if (m_pCurValues)
+ {
+ ValueMap::iterator i = m_pCurValues->find(setting);
+
+ if (i != m_pCurValues->end())
+ {
+ return (i->second == muT("y"));
+ }
+ }
+
+ return errorValue;
+}
+
+int SettingsTree::readInt(const mu_text* setting, int errorValue) const
+{
+ if (m_pCurValues)
+ {
+ ValueMap::iterator i = m_pCurValues->find(setting);
+
+ if (i != m_pCurValues->end())
+ {
+ return _ttoi(i->second.c_str());
+ }
+ }
+
+ return errorValue;
+}
+
+int SettingsTree::readIntRanged(const mu_text* setting, int errorValue, int minValue, int maxValue) const
+{
+ int value = readInt(setting, errorValue);
+
+ if (minValue <= value && value <= maxValue)
+ {
+ return value;
+ }
+ else
+ {
+ return errorValue;
+ }
+}
+
+ext::string SettingsTree::readStr(const mu_text* setting, const mu_text* errorValue) const
+{
+ if (m_pCurValues)
+ {
+ ValueMap::iterator i = m_pCurValues->find(setting);
+
+ if (i != m_pCurValues->end())
+ {
+ return i->second;
+ }
+ }
+
+ return errorValue;
+}
+
+void SettingsTree::writeBool(const mu_text* setting, bool value)
+{
+ makeKeyValid();
+
+ (*m_pCurValues)[setting] = value ? muT("y") : muT("n");
+}
+
+void SettingsTree::writeInt(const mu_text* setting, int value)
+{
+ makeKeyValid();
+
+ (*m_pCurValues)[setting] = utils::intToString(value);
+}
+
+void SettingsTree::writeStr(const mu_text* setting, const mu_text* value)
+{
+ makeKeyValid();
+
+ (*m_pCurValues)[setting] = value;
+}
+
+bool SettingsTree::hasSetting(const mu_text* setting) const
+{
+ if (m_pCurValues)
+ {
+ ValueMap::const_iterator i = m_pCurValues->find(setting);
+
+ if (i != m_pCurValues->end())
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool SettingsTree::delSetting(const mu_text* setting)
+{
+ if (m_pCurValues)
+ {
+ ValueMap::iterator i = m_pCurValues->find(setting);
+
+ if (i != m_pCurValues->end())
+ {
+ m_pCurValues->erase(i);
+
+ if (m_pCurValues->size() == 0)
+ {
+ m_Keys.erase(m_CurKey);
+ m_pCurValues = NULL;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/plugins/HistoryStats/src/settingstree.h b/plugins/HistoryStats/src/settingstree.h
new file mode 100644
index 0000000000..5f7bc4b07f
--- /dev/null
+++ b/plugins/HistoryStats/src/settingstree.h
@@ -0,0 +1,49 @@
+#if !defined(HISTORYSTATS_GUARD_SETTINGSTREE_H)
+#define HISTORYSTATS_GUARD_SETTINGSTREE_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include <map>
+
+class SettingsTree
+ : private pattern::NotCopyable<SettingsTree>
+{
+private:
+ typedef std::map<ext::string, ext::string> ValueMap;
+ typedef std::map<ext::string, ValueMap> KeyMap;
+
+private:
+ KeyMap m_Keys;
+ ext::string m_CurKey;
+ ValueMap* m_pCurValues;
+
+private:
+ void makeKeyValid();
+
+public:
+ explicit SettingsTree();
+ explicit SettingsTree(const ext::string& config);
+
+ void clear();
+
+ void fromString(const ext::string& config);
+ ext::string toString() const;
+
+ void setKey(const mu_text* key);
+ const ext::string& getKey() const { return m_CurKey; }
+
+ bool readBool(const mu_text* setting, bool errorValue) const;
+ int readInt(const mu_text* setting, int errorValue) const;
+ int readIntRanged(const mu_text* setting, int errorValue, int minValue, int maxValue) const;
+ ext::string readStr(const mu_text* setting, const mu_text* errorValue) const;
+
+ void writeBool(const mu_text* setting, bool value);
+ void writeInt(const mu_text* setting, int value);
+ void writeStr(const mu_text* setting, const mu_text* value);
+
+ bool hasSetting(const mu_text* setting) const;
+ bool delSetting(const mu_text* setting);
+};
+
+#endif // HISTORYSTATS_GUARD_SETTINGSTREE_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/statistic.cpp b/plugins/HistoryStats/src/statistic.cpp
new file mode 100644
index 0000000000..ca93c8c9fa
--- /dev/null
+++ b/plugins/HistoryStats/src/statistic.cpp
@@ -0,0 +1,1676 @@
+#include "_globals.h"
+#include "statistic.h"
+
+#include <algorithm>
+
+#include "utils.h"
+#include "utf8buffer.h"
+#include "resource.h"
+#include "column.h"
+#include "main.h"
+#include "mirandahistory.h"
+#include "mirandacontact.h"
+
+/*
+ * Statistic
+ */
+
+bool Statistic::m_bRunning = false;
+
+void Statistic::prepareColumns()
+{
+ m_ActiveCols.clear();
+ m_AcquireCols.clear();
+ m_TransformCols.clear();
+
+ std::map<ext::string, int> dataGUIDToSlot;
+
+ bool bOutputPNG = m_Settings.isPNGOutputActiveAndAvailable();
+
+ upto_each_(i, m_Settings.countCol())
+ {
+ Column* pCol = m_Settings.getCol(i);
+
+ if (pCol->isEnabled())
+ {
+ int restrictions = pCol->configGetRestrictions(NULL);
+
+ // MEMO: checks for columns having no HTML-only support
+ if (!bOutputPNG && !(restrictions & Column::crHTMLMask))
+ {
+ continue;
+ }
+
+ m_ActiveCols.push_back(pCol);
+
+ pCol->setHelpers(this, &m_Settings, &m_CharMapper);
+
+ if (pCol->getFeatures() & Column::cfAcquiresData)
+ {
+ ext::string dataGUID = pCol->contactDataGetUID();
+
+ std::map<ext::string, int>::iterator g2s = dataGUIDToSlot.find(dataGUID);
+
+ if (g2s == dataGUIDToSlot.end())
+ {
+ dataGUIDToSlot[dataGUID] = m_nNextSlot;
+ m_nNextSlot++;
+
+ m_AcquireCols.push_back(pCol);
+ }
+
+ pCol->contactDataSlotAssign(dataGUIDToSlot[dataGUID]);
+
+ if (pCol->getFeatures() & Column::cfTransformsData)
+ {
+ m_TransformCols.push_back(pCol);
+
+ pCol->contactDataTransformSlotAssign(m_nNextSlot++);
+ }
+ }
+ }
+ }
+}
+
+void Statistic::prepareContactData(Contact& contact)
+{
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataPrepare(contact);
+ }
+}
+
+void Statistic::freeContactData(Contact& contact)
+{
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataFree(contact);
+ }
+
+ iter_each_(std::vector<Column*>, i, m_TransformCols)
+ {
+ (*i)->contactDataFree(contact);
+ }
+}
+
+void Statistic::mergeContactData(Contact& contact, const Contact& include)
+{
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataMerge(contact, include);
+ }
+}
+
+void Statistic::transformContactData(Contact& contact)
+{
+ iter_each_(std::vector<Column*>, i, m_TransformCols)
+ {
+ (*i)->contactDataTransform(contact);
+ }
+}
+
+Contact& Statistic::addContact(const ext::string& nick, const ext::string& protoDisplayName, const ext::string& groupName, int nSources)
+{
+ Contact* pContact = new Contact(this, m_nNextSlot, nick, protoDisplayName, groupName, 1, nSources);
+ prepareContactData(*pContact);
+
+ m_Contacts.push_back(pContact);
+
+ return *pContact;
+}
+
+const Contact& Statistic::getContact(int index) const
+{
+ assert(index >= 0 && index < m_Contacts.size());
+
+ return *m_Contacts[index];
+}
+
+DWORD Statistic::getFirstTime()
+{
+ if (!m_bHistoryTimeAvailable)
+ {
+ // put _all_ available contacts (including omitted/total) in one list
+ ContactListC l;
+
+ upto_each_(i, countContacts())
+ {
+ l.push_back(&getContact(i));
+ }
+
+ if (hasOmitted())
+ {
+ l.push_back(&getOmitted());
+ }
+
+ if (hasTotals())
+ {
+ l.push_back(&getTotals());
+ }
+
+ if (l.size() > 0)
+ {
+ DWORD nFirstTime = con::MaxDateTime, nLastTime = con::MinDateTime;
+
+ citer_each_(Statistic::ContactListC, c, l)
+ {
+ if ((*c)->isFirstLastTimeValid())
+ {
+ nFirstTime = min(nFirstTime, (*c)->getFirstTime());
+ nLastTime = max(nLastTime, (*c)->getLastTime());
+ }
+ }
+
+ if (nFirstTime == con::MaxDateTime && nLastTime == con::MinDateTime)
+ {
+ m_nFirstTime = m_nLastTime = 0;
+ }
+ else
+ {
+ m_nFirstTime = nFirstTime;
+ m_nLastTime = nLastTime;
+ }
+ }
+ else
+ {
+ m_nFirstTime = m_nLastTime = 0;
+ }
+
+ // mark data as available
+ m_bHistoryTimeAvailable = true;
+ }
+
+ return m_nFirstTime;
+}
+
+DWORD Statistic::getLastTime()
+{
+ if (!m_bHistoryTimeAvailable)
+ {
+ // trigger calculation
+ getFirstTime();
+ }
+
+ return m_nLastTime;
+}
+
+DWORD Statistic::getHistoryTime()
+{
+ if (!m_bHistoryTimeAvailable)
+ {
+ // trigger calculation
+ getFirstTime();
+ }
+
+ return m_nLastTime - m_nFirstTime;
+}
+
+ext::string Statistic::createFile(const ext::string& desiredName)
+{
+ if (!m_Settings.m_OverwriteAlways && utils::fileExists(desiredName))
+ {
+ mu_text tempBuf[MAX_PATH];
+
+ UINT nUnique = GetTempFileName(m_TempPath.c_str(), muT("his"), 0, tempBuf);
+
+ if (nUnique == 0)
+ {
+ abort();
+ }
+
+ ext::string tempName = tempBuf;
+
+ m_ConflictingFiles.push_back(std::make_pair(desiredName, tempName));
+
+ return tempName;
+ }
+
+ ext::string desiredPath = utils::extractPath(desiredName);
+
+ if (!utils::pathExists(desiredPath))
+ {
+ if (!utils::createPath(desiredPath))
+ {
+ m_ErrorText = ext::str(ext::kformat(i18n(muT("HistoryStats couldn't create a required folder (#{folder}).\r\n\r\nPlease check the output filename and additional output folder you have chosen for correctness. Additionally, please check whether the file, folder, and/or disk is writable.")))
+ % muT("#{folder}") * desiredPath);
+ }
+ }
+
+ return desiredName;
+}
+
+bool Statistic::newFile(const mu_text* fileExt, ext::string& writeFile, ext::string& finalURL)
+{
+ ++m_nLastFileNr;
+
+ finalURL = m_OutputPrefix + utils::intToString(m_nLastFileNr) + fileExt;
+ writeFile = createFile(m_OutputPath + finalURL);
+
+ // convert relative filename to relative URL
+ utils::replaceAllInPlace(finalURL, muT("\\"), muT("/"));
+
+ return true;
+}
+
+bool Statistic::newFilePNG(Canvas& canvas, ext::string& finalURL)
+{
+ Canvas::Digest digest;
+
+ if (!canvas.getDigest(digest))
+ {
+ return false;
+ }
+
+ ImageMap::const_iterator i = m_Images.find(digest);
+
+ if (i == m_Images.end())
+ {
+ ext::string writeFile;
+
+ if (newFilePNG(writeFile, finalURL))
+ {
+ canvas.writePNG(writeFile.c_str());
+ m_Images.insert(std::make_pair(digest, finalURL));
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ finalURL = i->second;
+
+ return true;
+ }
+}
+
+void Statistic::handleAddMessage(Contact& contact, Message& msg)
+{
+ contact.addMessage(msg);
+
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataAcquireMessage(contact, msg);
+ }
+}
+
+void Statistic::handleAddChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration)
+{
+ if (duration >= m_Settings.m_ChatSessionMinDur)
+ {
+ contact.addChat(bOutgoing, localTimestampStarted, duration);
+
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataAcquireChat(contact, bOutgoing, localTimestampStarted, duration);
+ }
+ }
+}
+
+INT_PTR CALLBACK Statistic::staticProgressProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+ Statistic* pStats = reinterpret_cast<Statistic*>(GetWindowLong(hDlg, GWLP_USERDATA));
+
+ switch (msg)
+ {
+ case WM_INITDIALOG:
+ TranslateDialogDefault(hDlg);
+ SendMessage(hDlg, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_HISTORYSTATS))));
+ return TRUE;
+
+ case WM_DESTROY:
+ PostQuitMessage(0);
+ return TRUE;
+
+ case WM_COMMAND:
+ if (LOWORD(wParam) == IDOK)
+ {
+ EnableWindow(GetDlgItem(hDlg, IDCANCEL), FALSE);
+ SetEvent(pStats->m_hCancelEvent);
+ }
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void Statistic::setProgressMax(bool bSub, int max)
+{
+ SendDlgItemMessage(m_hWndProgress, bSub ? IDC_SUBBAR : IDC_MAINBAR, PBM_SETPOS, (WPARAM) 0, (LPARAM) 0);
+ SendDlgItemMessage(m_hWndProgress, bSub ? IDC_SUBBAR : IDC_MAINBAR, PBM_SETRANGE, (WPARAM) 0, (LPARAM) MAKELPARAM(0, max));
+
+ SetDlgItemText(m_hWndProgress, bSub ? IDC_SUBPERCENT : IDC_MAINPERCENT, (max > 0) ? muT("0%") : muT(""));
+
+ if (!bSub)
+ {
+ setProgressMax(true, 0);
+ setProgressLabel(true, muT(""));
+ }
+}
+
+void Statistic::setProgressLabel(bool bSub, const ext::string& label)
+{
+ SetDlgItemText(m_hWndProgress, bSub ? IDC_SUBTEXT : IDC_MAINTEXT, label.c_str());
+
+ if (!bSub)
+ {
+ setProgressMax(true, 0);
+ setProgressLabel(true, muT(""));
+ }
+}
+
+void Statistic::stepProgress(bool bSub, int step /* = 1 */)
+{
+ int pos = SendDlgItemMessage(m_hWndProgress, bSub ? IDC_SUBBAR : IDC_MAINBAR, PBM_GETPOS, (WPARAM) 0, (LPARAM) 0);
+ int max = SendDlgItemMessage(m_hWndProgress, bSub ? IDC_SUBBAR : IDC_MAINBAR, PBM_GETRANGE, (WPARAM) FALSE, (LPARAM) NULL);
+
+ pos += step;
+
+ SendDlgItemMessage(m_hWndProgress, bSub ? IDC_SUBBAR : IDC_MAINBAR, PBM_SETPOS, (WPARAM) pos, (LPARAM) 0);
+ SetDlgItemText(m_hWndProgress, bSub ? IDC_SUBPERCENT : IDC_MAINPERCENT, utils::ratioToPercent(pos, max).c_str());
+
+ if (!bSub)
+ {
+ setProgressMax(true, 0);
+ setProgressLabel(true, muT(""));
+ }
+}
+
+bool Statistic::stepInit()
+{
+ // file management
+ mu_text tempPath[MAX_PATH];
+ int nRes = GetTempPath(MAX_PATH, tempPath);
+
+ if (nRes > 0)
+ {
+ m_TempPath.assign(tempPath, nRes);
+ }
+
+ m_OutputFile = m_Settings.getOutputFile(getTimeStarted());
+ m_OutputPath = utils::extractPath(m_OutputFile);
+ m_OutputPrefix = m_Settings.getOutputPrefix(getTimeStarted());
+
+ // init column info
+ prepareColumns();
+
+ // figure out minimum/maximum date/time to include
+ m_TimeMin = 0;
+ m_TimeMax = 0xFFFFFFFF;
+
+ if (m_Settings.m_IgnoreOld != 0)
+ {
+ m_TimeMin = getTimeStarted() - 86400 * m_Settings.m_IgnoreOld;
+ }
+
+ if (m_Settings.getIgnoreBefore() != 0)
+ {
+ if (m_Settings.m_IgnoreOld != 0)
+ {
+ m_TimeMin = max(m_TimeMin, m_Settings.getIgnoreBefore());
+ }
+ else
+ {
+ m_TimeMin = m_Settings.getIgnoreBefore();
+ }
+ }
+
+ if (m_Settings.getIgnoreAfter() != 0)
+ {
+ m_TimeMax = m_Settings.getIgnoreAfter() + 86399;
+ }
+
+ return true;
+}
+
+bool Statistic::stepReadDB()
+{
+ if (shouldTerminate())
+ {
+ return false;
+ }
+
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataBeginAcquire();
+ }
+
+ // prepare some data
+ MirandaHistory history(m_Settings);
+
+ setProgressMax(true, history.getContactCount());
+
+ upto_each_(contactIndex, history.getContactCount())
+ {
+ MirandaContact& hisContact = history.getContact(contactIndex);
+
+ setProgressLabel(true, hisContact.getNick());
+
+ Contact& curContact = addContact(hisContact.getNick(), hisContact.getProtocol(), hisContact.getGroup(), hisContact.getSources().size());
+
+ // signal begin of history for this contact
+ hisContact.beginRead();
+ curContact.beginMessages();
+
+ // init data for chat detection
+ DWORD lastAddedTime = 0;
+ DWORD chatStartTime = 0;
+ bool bChatOutgoing = false;
+ Message curMsg(m_Settings.m_FilterRawRTF && RTFFilter::available(), m_Settings.m_FilterBBCodes);
+
+ // iterate through all events
+ while (hisContact.hasNext())
+ {
+ const DBEVENTINFO& dbei = hisContact.getNext();
+
+ bool bOutgoing = bool_(dbei.flags & DBEF_SENT);
+
+ // only messages, no URLs, files or anything else
+ // filter logged status messages from tabSRMM
+ if (dbei.eventType == etMessage)
+ {
+ // convert to local time (everything in this plugin is done in local time)
+ DWORD localTimestamp = utils::toLocalTime(dbei.timestamp);
+
+ if (localTimestamp >= m_TimeMin && localTimestamp <= m_TimeMax)
+ {
+ if (dbei.flags & DBEF_UTF)
+ {
+ mu_ansi* pUTF8Text = reinterpret_cast<mu_ansi*>(dbei.pBlob);
+ int nUTF8Len = utils::getUTF8Len(pUTF8Text);
+
+ curMsg.assignTextFromUTF8(pUTF8Text, nUTF8Len);
+ }
+ else
+ {
+ mu_ansi* pAnsiText = reinterpret_cast<mu_ansi*>(dbei.pBlob);
+ int nAnsiLenP1 = ext::a::strfunc::len(pAnsiText) + 1;
+
+#if defined(MU_WIDE)
+ mu_wide* pWideText = reinterpret_cast<mu_wide*>(pAnsiText + nAnsiLenP1);
+ int nWideLen = 0;
+ int nWideMaxLen = (dbei.cbBlob - nAnsiLenP1) / sizeof(mu_wide);
+
+ if (dbei.cbBlob >= nAnsiLenP1 * 3)
+ {
+ for (int i = 0; i < nWideMaxLen; ++i)
+ {
+ if (!pWideText[i])
+ {
+ nWideLen = i;
+ break;
+ }
+ }
+ }
+
+ if (nWideLen > 0 && nWideLen < nAnsiLenP1)
+ {
+ curMsg.assignText(pWideText, nWideLen);
+ }
+ else
+ {
+ curMsg.assignText(pAnsiText, nAnsiLenP1 - 1);
+ }
+#else // MU_WIDE
+ curMsg.assignText(pAnsiText, nAnsiLenP1 - 1);
+#endif // MU_WIDE
+ }
+
+ curMsg.assignInfo(bOutgoing, localTimestamp);
+
+ // handle messages
+ handleAddMessage(curContact, curMsg);
+
+ // handle chats
+ if (localTimestamp - lastAddedTime >= (DWORD) m_Settings.m_ChatSessionTimeout || lastAddedTime == 0)
+ {
+ // new chat started
+ if (chatStartTime != 0)
+ {
+ handleAddChat(curContact, bChatOutgoing, chatStartTime, lastAddedTime - chatStartTime);
+ }
+
+ chatStartTime = localTimestamp;
+ bChatOutgoing = bOutgoing;
+ }
+
+ lastAddedTime = localTimestamp;
+ }
+ }
+
+ // non-message events
+ if (dbei.eventType != etMessage)
+ {
+ curContact.addEvent(dbei.eventType, bOutgoing);
+ }
+
+ hisContact.readNext();
+ }
+
+ // post processing for chat detection
+ if (chatStartTime != 0)
+ {
+ handleAddChat(curContact, bChatOutgoing, chatStartTime, lastAddedTime - chatStartTime);
+ }
+
+ // signal end of history for this contact
+ curContact.endMessages();
+ hisContact.endRead();
+
+ stepProgress(true);
+
+ if (shouldTerminate())
+ {
+ return false;
+ }
+ }
+
+ iter_each_(std::vector<Column*>, i, m_AcquireCols)
+ {
+ (*i)->contactDataEndAcquire();
+ }
+
+ return true;
+}
+
+bool Statistic::stepRemoveContacts()
+{
+ if (!m_Settings.m_RemoveEmptyContacts && !m_Settings.m_RemoveOutChatsZero && !m_Settings.m_RemoveInChatsZero)
+ {
+ return true;
+ }
+
+ if (shouldTerminate())
+ {
+ return false;
+ }
+
+ vector_each_(i, m_Contacts)
+ {
+ bool bRemove = false;
+ Contact* pCur = m_Contacts[i];
+
+ if (!bRemove && m_Settings.m_RemoveEmptyContacts)
+ {
+ bRemove = (pCur->getTotalMessages() == 0);
+ }
+
+ if (!bRemove && m_Settings.m_RemoveOutChatsZero)
+ {
+ bRemove = (pCur->getOutChats() == 0 && (!m_Settings.m_RemoveOutBytesZero || pCur->getOutBytes() == 0));
+ }
+
+ if (!bRemove && m_Settings.m_RemoveInChatsZero)
+ {
+ bRemove = (pCur->getInChats() == 0 && (!m_Settings.m_RemoveInBytesZero || pCur->getInBytes() == 0));
+ }
+
+ if (bRemove)
+ {
+ freeContactData(*pCur);
+ delete pCur;
+
+ m_Contacts.erase(m_Contacts.begin() + i);
+ --i;
+ }
+ }
+
+ return true;
+}
+
+bool Statistic::stepSortContacts()
+{
+ if (shouldTerminate())
+ {
+ return false;
+ }
+
+ ContactCompareBase cmpLast;
+ ContactCompareStr cmpName(&cmpLast, &Contact::getNick);
+
+ int cmpDepth = 3;
+
+ upto_each_(i, Settings::cNumSortLevels)
+ {
+ if (m_Settings.m_Sort[i].by == Settings::skNothing)
+ {
+ cmpDepth = i;
+ break;
+ }
+ }
+
+ ContactCompareBase** ppCmps = new ContactCompareBase*[cmpDepth];
+
+ ContactCompareBase* pCmp = NULL;
+ ContactCompareBase* pPrev = &cmpName;
+
+ for (int i = cmpDepth - 1; i >= 0; --i) {
+ switch (m_Settings.m_Sort[i].by) {
+ case Settings::skNick:
+ pCmp = new ContactCompareStr(pPrev, &Contact::getNick);
+ break;
+
+ case Settings::skProtocol:
+ pCmp = new ContactCompareStr(pPrev, &Contact::getProtocol);
+ break;
+
+ case Settings::skGroup:
+ pCmp = new ContactCompareStr(pPrev, &Contact::getGroup);
+ break;
+
+ case Settings::skBytesOut:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getOutBytes);
+ break;
+
+ case Settings::skBytesIn:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getInBytes);
+ break;
+
+ case Settings::skBytesTotal:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getTotalBytes);
+ break;
+
+ case Settings::skMessagesOut:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getOutMessages);
+ break;
+
+ case Settings::skMessagesIn:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getOutMessages);
+ break;
+
+ case Settings::skMessagesTotal:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getTotalMessages);
+ break;
+
+ case Settings::skChatsOut:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getOutChats);
+ break;
+
+ case Settings::skChatsIn:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getInChats);
+ break;
+
+ case Settings::skChatsTotal:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getTotalChats);
+ break;
+
+ case Settings::skChatDurationTotal:
+ pCmp = new ContactCompare<DWORD>(pPrev, &Contact::getChatDurSum);
+ break;
+
+ case Settings::skTimeOfFirstMessage:
+ pCmp = new ContactCompare<DWORD>(pPrev, &Contact::getFirstTime);
+ break;
+
+ case Settings::skTimeOfLastMessage:
+ pCmp = new ContactCompare<DWORD>(pPrev, &Contact::getLastTime);
+ break;
+
+ case Settings::skBytesOutAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getOutBytesAvg);
+ break;
+
+ case Settings::skBytesInAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getInBytesAvg);
+ break;
+
+ case Settings::skBytesTotalAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getTotalBytesAvg);
+ break;
+
+ case Settings::skMessagesOutAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getOutMessagesAvg);
+ break;
+
+ case Settings::skMessagesInAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getOutMessagesAvg);
+ break;
+
+ case Settings::skMessagesTotalAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getTotalMessagesAvg);
+ break;
+
+ case Settings::skChatsOutAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getOutChatsAvg);
+ break;
+
+ case Settings::skChatsInAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getInChatsAvg);
+ break;
+
+ case Settings::skChatsTotalAvg:
+ pCmp = new ContactCompare<double>(pPrev, &Contact::getTotalChatsAvg);
+ break;
+
+ case Settings::skChatDurationMin:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getChatDurMinForSort);
+ break;
+
+ case Settings::skChatDurationAvg:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getChatDurAvgForSort);
+ break;
+
+ case Settings::skChatDurationMax:
+ pCmp = new ContactCompare<int>(pPrev, &Contact::getChatDurMaxForSort);
+ break;
+ }
+
+ pCmp->setDir(m_Settings.m_Sort[i].asc);
+
+ ppCmps[i] = pPrev = pCmp;
+ pCmp = NULL;
+ }
+
+ std::sort(m_Contacts.begin(), m_Contacts.end(), ContactCompareOp(ppCmps[0]));
+
+ upto_each_(i, cmpDepth)
+ {
+ delete ppCmps[i];
+ }
+
+ delete[] ppCmps;
+
+ return true;
+}
+
+bool Statistic::stepPreOmitContacts()
+{
+ if (shouldTerminate())
+ return false;
+
+ iter_each_(std::vector<Column*>, i, m_ActiveCols)
+ {
+ (*i)->columnDataBeforeOmit();
+ }
+
+ return true;
+}
+
+bool Statistic::stepOmitContacts()
+{
+ if (!m_Settings.m_OmitContacts)
+ return true;
+
+ if (shouldTerminate())
+ return false;
+
+ m_pOmitted = new Contact(this, m_nNextSlot, muT(""), muT(""), muT(""), 0, 0);
+ prepareContactData(*m_pOmitted);
+
+ // omit depending on some value
+ if (m_Settings.m_OmitByValue) {
+ static const struct {
+ int type; // 0 = int, 1 = double, 2 = DWORD
+ double factor; // factor to multiply function output with
+ int (Contact::*int_fn)() const;
+ double (Contact::*double_fn)() const;
+ DWORD (Contact::*DWORD_fn)() const;
+ } valueMap[] = {
+ { 0, 1.0, &Contact::getInBytes , 0 , 0 },
+ { 0, 1.0, &Contact::getOutBytes , 0 , 0 },
+ { 0, 1.0, &Contact::getTotalBytes , 0 , 0 },
+ { 1, 604800.0, 0 , &Contact::getInBytesAvg , 0 },
+ { 1, 604800.0, 0 , &Contact::getOutBytesAvg , 0 },
+ { 1, 604800.0, 0 , &Contact::getTotalBytesAvg , 0 },
+ { 0, 1.0, &Contact::getInMessages , 0 , 0 },
+ { 0, 1.0, &Contact::getOutMessages , 0 , 0 },
+ { 0, 1.0, &Contact::getTotalMessages, 0 , 0 },
+ { 1, 604800.0, 0 , &Contact::getInMessagesAvg , 0 },
+ { 1, 604800.0, 0 , &Contact::getOutMessagesAvg , 0 },
+ { 1, 604800.0, 0 , &Contact::getTotalMessagesAvg, 0 },
+ { 0, 1.0, &Contact::getInChats , 0 , 0 },
+ { 0, 1.0, &Contact::getOutChats , 0 , 0 },
+ { 0, 1.0, &Contact::getTotalChats , 0 , 0 },
+ { 1, 604800.0, 0 , &Contact::getInChatsAvg , 0 },
+ { 1, 604800.0, 0 , &Contact::getOutChatsAvg , 0 },
+ { 1, 604800.0, 0 , &Contact::getTotalChatsAvg , 0 },
+ { 2, 1/3600.0, 0 , 0 , &Contact::getChatDurSum },
+ };
+
+ int valueKey = m_Settings.m_OmitByValueData;
+ double fLimit = static_cast<double>(m_Settings.m_OmitByValueLimit) / valueMap[valueKey].factor;
+
+ for (int i = m_Contacts.size() - 1; i >= 0; --i) {
+ Contact& cur = *m_Contacts[i];
+
+ bool bDoOmit = false;
+
+ switch (valueMap[valueKey].type) {
+ case 0:
+ bDoOmit = (static_cast<double>((cur.*valueMap[valueKey].int_fn)()) < fLimit);
+ break;
+
+ case 1:
+ bDoOmit = ((cur.*valueMap[valueKey].double_fn)() < fLimit);
+ break;
+
+ case 2:
+ bDoOmit = (static_cast<double>((cur.*valueMap[valueKey].DWORD_fn)()) < fLimit);
+ break;
+ }
+
+ if (bDoOmit) {
+ if (m_Settings.m_OmittedInTotals && m_Settings.m_CalcTotals || m_Settings.m_OmittedInExtraRow) {
+ m_pOmitted->merge(cur);
+ mergeContactData(*m_pOmitted, cur);
+
+ m_bActuallyOmitted = true;
+ }
+
+ freeContactData(cur);
+ delete m_Contacts[i];
+
+ m_Contacts.erase(m_Contacts.begin() + i);
+ }
+
+ if (shouldTerminate())
+ return false;
+ }
+ }
+
+ // omit depending on message time
+ if (m_Settings.m_OmitByTime) {
+ for (int i = m_Contacts.size() - 1; i >= 0; --i) {
+ Contact& cur = *m_Contacts[i];
+
+ if (!cur.isFirstLastTimeValid() || (getTimeStarted() > cur.getLastTime() && getTimeStarted() - cur.getLastTime() > m_Settings.m_OmitByTimeDays * 86400)) {
+ if (m_Settings.m_OmittedInTotals && m_Settings.m_CalcTotals || m_Settings.m_OmittedInExtraRow) {
+ m_pOmitted->merge(cur);
+ mergeContactData(*m_pOmitted, cur);
+
+ m_bActuallyOmitted = true;
+ }
+
+ freeContactData(cur);
+ delete m_Contacts[i];
+
+ m_Contacts.erase(m_Contacts.begin() + i);
+ }
+
+ if (shouldTerminate())
+ return false;
+ }
+ }
+
+ // omit depending on rank
+ if (m_Settings.m_OmitByRank) {
+ while (m_Contacts.size() > m_Settings.m_OmitNumOnTop) {
+ Contact& cur = *m_Contacts.back();
+
+ if (m_Settings.m_OmittedInTotals && m_Settings.m_CalcTotals || m_Settings.m_OmittedInExtraRow) {
+ m_pOmitted->merge(cur);
+ mergeContactData(*m_pOmitted, cur);
+
+ m_bActuallyOmitted = true;
+ }
+
+ freeContactData(cur);
+ delete m_Contacts.back();
+
+ m_Contacts.pop_back();
+
+ if (shouldTerminate())
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool Statistic::stepCalcTotals()
+{
+ if (!m_Settings.m_CalcTotals)
+ return true;
+
+ if (shouldTerminate())
+ return false;
+
+ m_pTotals = new Contact(this, m_nNextSlot, muT(""), muT(""), muT(""), 0, 0);
+ prepareContactData(*m_pTotals);
+
+ setProgressMax(true, m_Contacts.size() + 1);
+
+ // normal contacts
+ vector_each_(i, m_Contacts)
+ {
+ Contact& curContact = *m_Contacts[i];
+
+ setProgressLabel(true, curContact.getNick());
+
+ m_pTotals->merge(curContact);
+ mergeContactData(*m_pTotals, curContact);
+
+ stepProgress(true);
+
+ if (shouldTerminate())
+ return false;
+ }
+
+ // omitted contacts
+ setProgressLabel(true, i18n(muT("Omitted contacts")));
+
+ if (m_Settings.m_OmitContacts && m_Settings.m_OmittedInTotals && m_bActuallyOmitted) {
+ m_pTotals->merge(*m_pOmitted);
+ mergeContactData(*m_pTotals, *m_pOmitted);
+ }
+
+ stepProgress(true);
+
+ return true;
+}
+
+bool Statistic::stepPostOmitContacts()
+{
+ if (shouldTerminate())
+ {
+ return false;
+ }
+
+ iter_each_(std::vector<Column*>, i, m_ActiveCols)
+ {
+ (*i)->columnDataAfterOmit();
+ }
+
+ return true;
+}
+
+bool Statistic::stepTransformData()
+{
+ if (shouldTerminate())
+ return false;
+
+ setProgressMax(true, m_Contacts.size() + 2);
+
+ // normal contacts
+ vector_each_(i, m_Contacts)
+ {
+ Contact& curContact = *m_Contacts[i];
+
+ setProgressLabel(true, curContact.getNick());
+ transformContactData(curContact);
+ stepProgress(true);
+
+ if (shouldTerminate())
+ return false;
+ }
+
+ // omitted contacts
+ setProgressLabel(true, i18n(muT("Omitted contacts")));
+
+ if (m_bActuallyOmitted)
+ transformContactData(*m_pOmitted);
+
+ stepProgress(true);
+
+ // totals
+ setProgressLabel(true, i18n(muT("Totals")));
+
+ if (m_Settings.m_CalcTotals)
+ transformContactData(*m_pTotals);
+
+ stepProgress(true);
+ return true;
+}
+
+bool Statistic::stepWriteHTML()
+{
+ if (shouldTerminate())
+ return false;
+
+ bool bInterrupted = false;
+
+ int j;
+
+ /*
+ * Init output.
+ */
+
+ setProgressMax(true, countContacts() + 2);
+
+ /*
+ * Create output stream.
+ */
+
+ ext::a::ofstream ofs(utils::toA(createFile(m_OutputFile)).c_str());
+
+ if (!ofs.good()) {
+ m_ErrorText = ext::str(ext::kformat(i18n(muT("HistoryStats couldn't open the output file (#{file}) for write access.\r\n\r\nPlease check the output filename you have chosen for correctness. Additionally, please check whether the file, folder, and/or disk is writable.")))
+ % muT("#{file}") * m_OutputFile);
+ return false;
+ }
+
+ UTF8Buffer utf8_buf(ofs);
+ ext::ostream tos(&utf8_buf);
+
+ /*
+ * Inform active columns about beginning output.
+ */
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputBegin();
+ }
+
+ /*
+ * Output HTML init sequence.
+ */
+
+ std::set<ext::string> additionalCSSSelectors;
+ std::vector<ext::string> additionalCSS;
+ Column::IDProvider idProvider;
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols) {
+ Column::StyleList cssList = (*col)->outputGetAdditionalStyles(idProvider);
+
+ iter_each_(Column::StyleList, css, cssList) {
+ if (additionalCSSSelectors.find(css->first) == additionalCSSSelectors.end()) {
+ additionalCSS.push_back(css->first + muT(" { ") + css->second + muT(" }"));
+ additionalCSSSelectors.insert(css->first);
+ }
+ }
+
+ }
+
+ additionalCSSSelectors.clear();
+
+ tos << muT("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">") << ext::endl
+ << muT("<html>") << ext::endl
+ << muT("<head>") << ext::endl
+ << muT("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />") << ext::endl
+ << muT("<meta name=\"generator\" content=\"HistoryStats " << utils::versionToDotted(m_Settings.m_VersionCurrent) << "\" />") << ext::endl
+ << muT("<title>")
+ << ext::kformat(i18n(muT("Statistics for #{nick} - HistoryStats"))) % muT("#{nick}") * utils::htmlEscape(m_Settings.m_OwnNick)
+ << muT("</title>") << ext::endl
+ << muT("<style type=\"text/css\">") << ext::endl;
+ tos << m_Settings.getDefaultStyleSheet();
+
+ iter_each_(std::vector<ext::string>, css, additionalCSS)
+ {
+ tos << *css << ext::endl;
+ }
+
+ tos << muT("</style>") << ext::endl
+ << muT("</head>") << ext::endl
+ << muT("<body><h1>")
+ << ext::kformat(i18n(muT("Statistics for #{nick}"))) % muT("#{nick}") * utils::htmlEscape(m_Settings.m_OwnNick)
+ << muT("</h1>") << ext::endl;
+ tos << muT("<table>") << ext::endl;
+
+ additionalCSS.clear();
+
+ /*
+ * Output header.
+ */
+
+ SIZE headerSize = { 0, 1 };
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ SIZE colSize = (*col)->outputMeasureHeader();
+
+ headerSize.cx += colSize.cx;
+ headerSize.cy = max(headerSize.cy, colSize.cy);
+ }
+
+ if (m_Settings.m_TableHeader)
+ {
+ for (j = 1; j <= headerSize.cy; j++)
+ {
+ tos << muT("<tr class=\"header\">") << ext::endl;
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputRenderHeader(tos, j, headerSize.cy);
+ }
+
+ tos << muT("</tr>") << ext::endl;
+ }
+ }
+
+ // stop if problem creating files/folders
+ if (!m_ErrorText.empty())
+ {
+ bInterrupted = true;
+ }
+
+ if (shouldTerminate())
+ {
+ bInterrupted = true;
+ }
+
+ /*
+ * Output contacts.
+ */
+
+ if (!bInterrupted)
+ {
+ upto_each_(i, countContacts())
+ {
+ tos << muT("<tr>") << ext::endl;
+
+ const Contact& curContact = getContact(i);
+
+ setProgressLabel(true, curContact.getNick());
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputRenderRow(tos, curContact, Column::asContact);
+ }
+
+ tos << muT("</tr>") << ext::endl;
+
+ if (shouldTerminate())
+ {
+ bInterrupted = true;
+ break;
+ }
+
+ // stop if problem creating files/folders
+ if (!m_ErrorText.empty())
+ {
+ bInterrupted = true;
+ break;
+ }
+
+ if (m_Settings.m_TableHeader && m_Settings.m_TableHeaderRepeat != 0 && ((i + 1) % m_Settings.m_TableHeaderRepeat == 0))
+ {
+ for (j = 1; j <= headerSize.cy; ++j)
+ {
+ tos << muT("<tr class=\"header\">") << ext::endl;
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputRenderHeader(tos, j, headerSize.cy);
+ }
+
+ tos << muT("</tr>") << ext::endl;
+ }
+ }
+
+ stepProgress(true);
+ }
+ } // !bInterrupted
+
+ /*
+ * Output omitted contacts.
+ */
+
+ if (!bInterrupted && m_Settings.m_OmitContacts && m_Settings.m_OmittedInExtraRow && m_bActuallyOmitted)
+ {
+ setProgressLabel(true, i18n(muT("Writing omitted contacts")));
+
+ const Contact& omittedContact = getOmitted();
+
+ tos << muT("<tr class=\"omitted\">") << ext::endl;
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputRenderRow(tos, omittedContact, Column::asOmitted);
+ }
+
+ tos << muT("</tr>") << ext::endl;
+
+ // stop if problem creating files/folders
+ if (!m_ErrorText.empty())
+ {
+ bInterrupted = true;
+ }
+
+ if (shouldTerminate())
+ {
+ bInterrupted = true;
+ }
+ }
+
+ stepProgress(true);
+
+ /*
+ * Output totals.
+ */
+
+ if (!bInterrupted && m_Settings.m_CalcTotals)
+ {
+ setProgressLabel(true, i18n(muT("Writing totals")));
+
+ const Contact& totalsContact = getTotals();
+
+ tos << muT("<tr class=\"totals\">") << ext::endl;
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputRenderRow(tos, totalsContact, Column::asTotal);
+ }
+
+ tos << muT("</tr>") << ext::endl;
+
+ stepProgress(true);
+
+ /*
+ * Finish output.
+ */
+
+ tos << muT("</table>") << ext::endl;
+
+ tos << muT("<div class=\"footer\">")
+ << ext::kformat(i18n(muT("Created with #{plugin} #{version} on #{date} at #{time}")))
+ % muT("#{plugin}") * muT("<a href=\"http://miranda.dark-passage.de/\">HistoryStats</a>")
+ % muT("#{version}") * utils::versionToDotted(m_Settings.m_VersionCurrent)
+ % muT("#{date}") * utils::htmlEscape(utils::timestampToDate(getTimeStarted()))
+ % muT("#{time}") * utils::htmlEscape(utils::timestampToTime(getTimeStarted()))
+ << muT("</div>") << ext::endl;
+
+ tos << muT("</body></html>") << ext::endl;
+ } // !bInterrupted
+
+ /*
+ * Inform active columns about ending output.
+ */
+
+ iter_each_(std::vector<Column*>, col, m_ActiveCols)
+ {
+ (*col)->outputEnd();
+ }
+
+ /*
+ * Close output stream.
+ */
+
+ tos.flush();
+ ofs.close();
+
+ /*
+ * Handle conflicting files.
+ */
+
+ if (bInterrupted)
+ {
+ iter_each_(ConflictingFiles, fi, m_ConflictingFiles)
+ {
+ DeleteFile(fi->second.c_str());
+ }
+
+ m_ConflictingFiles.clear();
+ }
+
+ if (m_ConflictingFiles.size() > 0)
+ {
+ int nResult = DialogBoxParam(
+ m_hInst,
+ MAKEINTRESOURCE(IDD_CONFLICT),
+ m_hWndProgress,
+ staticConflictProc,
+ reinterpret_cast<LPARAM>(&m_ConflictingFiles));
+
+ if (nResult == IDOK)
+ {
+ iter_each_(ConflictingFiles, fi, m_ConflictingFiles)
+ {
+ if (!MoveFileEx(fi->second.c_str(), fi->first.c_str(), MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING))
+ {
+ if (!MoveFile(fi->second.c_str(), fi->first.c_str()))
+ {
+ CopyFile(fi->second.c_str(), fi->first.c_str(), FALSE);
+ DeleteFile(fi->second.c_str());
+ }
+ }
+ }
+ }
+ else
+ {
+ iter_each_(ConflictingFiles, fi, m_ConflictingFiles)
+ {
+ DeleteFile(fi->second.c_str());
+ }
+ }
+
+ m_ConflictingFiles.clear();
+ }
+
+ /* don't do this, we don't want to delete a file we possibly never touched
+ if (bInterrupted)
+ {
+ // remove partialy generated file, if interrupted
+ DeleteFile(m_OutputFile.c_str());
+ }
+ */
+
+ return !bInterrupted;
+}
+
+Statistic::Statistic(const Settings& settings, InvocationSource invokedFrom, HINSTANCE hInst)
+ : m_Settings(settings),
+ m_CharMapper(m_Settings),
+ m_hInst(hInst),
+ m_hWndProgress(NULL),
+ m_hThreadPushEvent(NULL),
+ m_hCancelEvent(NULL),
+ m_InvokedFrom(invokedFrom),
+ m_pTotals(NULL),
+ m_pOmitted(NULL),
+ m_bActuallyOmitted(false),
+ m_nNextSlot(0),
+ m_nLastFileNr(0),
+ m_TimeMin(0),
+ m_TimeMax(0xFFFFFFFF),
+ m_bHistoryTimeAvailable(false),
+ m_nFirstTime(0),
+ m_nLastTime(0)
+{
+ m_TimeStarted = utils::toLocalTime(time(NULL));
+ m_MSecStarted = GetTickCount();
+ m_AverageMinTime = settings.m_AverageMinTime * 24 * 60 * 60; // calculate seconds from days
+}
+
+bool Statistic::createStatistics()
+{
+ /*
+ * Prepare event for cancel.
+ */
+ m_hCancelEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
+
+ if (m_hCancelEvent == NULL)
+ {
+ return false;
+ }
+
+ m_hWndProgress = CreateDialog(m_hInst, MAKEINTRESOURCE(IDD_PROGRESS), 0, staticProgressProc);
+
+ if (m_hWndProgress == NULL)
+ {
+ CloseHandle(m_hCancelEvent);
+ m_hCancelEvent = NULL;
+ return false;
+ }
+
+ SetWindowLong(m_hWndProgress, GWLP_USERDATA, reinterpret_cast<LONG>(this));
+
+ /*
+ * Init progress dialog.
+ */
+ utils::centerDialog(m_hWndProgress);
+ UpdateWindow(m_hWndProgress);
+
+ DWORD dwThreadID = 0;
+ HANDLE hThread = CreateThread(NULL, 0, threadProcSteps, this, 0, &dwThreadID);
+
+ bool bDone = false;
+ MSG msg;
+
+ while (!bDone)
+ {
+ while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
+ {
+ if (msg.message == WM_QUIT)
+ {
+ bDone = true;
+ break;
+ }
+
+ if (!IsDialogMessage(msg.hwnd, &msg))
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ }
+
+ if (bDone)
+ {
+ break;
+ }
+
+ DWORD result = MsgWaitForMultipleObjects(1, &hThread, FALSE, INFINITE, QS_ALLINPUT);
+
+ if (result == WAIT_OBJECT_0 + 1)
+ {
+ // there is a message in the queue
+ continue;
+ }
+ else
+ {
+ // thread is signaled, i.e. terminated
+ DestroyWindow(m_hWndProgress);
+ }
+ }
+
+ /*
+ * Get result from thread.
+ */
+ // bool bSuccess = createStatisticsSteps();
+ DWORD threadRes;
+ bool bSuccess = false;
+
+ if (GetExitCodeThread(hThread, &threadRes))
+ {
+ bSuccess = (threadRes == 0);
+ }
+
+ /*
+ * Cleanup.
+ */
+ CloseHandle(hThread);
+ CloseHandle(m_hCancelEvent);
+ m_hCancelEvent = NULL;
+ m_hWndProgress = NULL;
+
+ if (bSuccess)
+ {
+ /*
+ * Save last successfully created statistics
+ */
+ g_pSettings->setLastStatisticsFile(m_OutputFile.c_str());
+
+ /*
+ * Open afterwards, if requested.
+ */
+ bool bOpenAfterwards =
+ (m_InvokedFrom == fromOptions && m_Settings.m_AutoOpenOptions) ||
+ (m_InvokedFrom == fromStartup && m_Settings.m_AutoOpenStartup) ||
+ (m_InvokedFrom == fromMenu && m_Settings.m_AutoOpenMenu);
+
+ if (bOpenAfterwards)
+ {
+ m_Settings.openURL(m_OutputFile.c_str());
+ }
+ }
+
+ return bSuccess;
+}
+
+bool Statistic::createStatisticsSteps()
+{
+ static const struct {
+ bool (Statistic::*stepFn)();
+ mu_text* stepMsg;
+ } stepsInfo[] = {
+ { &Statistic::stepInit , I18N(muT("Initializing")) },
+ { &Statistic::stepReadDB , I18N(muT("Reading database")) },
+ { &Statistic::stepRemoveContacts , I18N(muT("Removing contacts")) },
+ { &Statistic::stepSortContacts , I18N(muT("Sorting contacts")) },
+ { &Statistic::stepPreOmitContacts , I18N(muT("Precollecting column data")) },
+ { &Statistic::stepOmitContacts , I18N(muT("Limiting number of contacts")) },
+ { &Statistic::stepCalcTotals , I18N(muT("Calculating totals")) },
+ { &Statistic::stepPostOmitContacts, I18N(muT("Postcollecting column data")) },
+ { &Statistic::stepTransformData , I18N(muT("Transforming data")) },
+ { &Statistic::stepWriteHTML , I18N(muT("Creating HTML")) }
+ };
+
+ setProgressMax(false, array_len(stepsInfo));
+
+ array_each_(i, stepsInfo)
+ {
+ setProgressLabel(false, i18n(stepsInfo[i].stepMsg));
+
+ if (!(this->*stepsInfo[i].stepFn)())
+ {
+ return false;
+ }
+
+ stepProgress(false);
+ }
+
+ /*
+ * Last step: We are done.
+ */
+ setProgressLabel(false, i18n(muT("Done")));
+
+ return true;
+}
+
+DWORD WINAPI Statistic::threadProc(LPVOID lpParameter)
+{
+ Statistic* pStats = reinterpret_cast<Statistic*>(lpParameter);
+
+ SetEvent(pStats->m_hThreadPushEvent);
+
+ // perform action
+ bool bSuccess = pStats->createStatistics();
+
+ // check for errors
+ if (!pStats->m_ErrorText.empty() && !mu::system::terminated())
+ {
+ MessageBox(
+ 0,
+ pStats->m_ErrorText.c_str(),
+ i18n(muT("HistoryStats - Error")),
+ MB_ICONERROR | MB_OK);
+ }
+
+ // free statistics
+ delete pStats;
+
+ m_bRunning = false;
+ return 0;
+}
+
+DWORD WINAPI Statistic::threadProcSteps(LPVOID lpParameter)
+{
+ Statistic* pStats = reinterpret_cast<Statistic*>(lpParameter);
+
+ if (pStats->m_Settings.m_ThreadLowPriority)
+ {
+ SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
+ }
+
+ bool bSuccess = pStats->createStatisticsSteps();
+
+ return (bSuccess ? 0 : 1);
+}
+
+INT_PTR CALLBACK Statistic::staticConflictProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ if (uMsg == WM_INITDIALOG)
+ {
+ TranslateDialogDefault(hDlg);
+
+ SendMessage(hDlg, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_HISTORYSTATS))));
+
+ utils::centerDialog(hDlg);
+
+ HWND hWndFiles = GetDlgItem(hDlg, IDC_FILES);
+ ConflictingFiles* pFiles = reinterpret_cast<ConflictingFiles*>(lParam);
+
+ LVCOLUMN lvc;
+
+ lvc.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT;
+ lvc.fmt = LVCFMT_LEFT;
+ lvc.cx = 400;
+ lvc.pszText = const_cast<mu_text*>(i18n(muT("Already existing file")));
+
+ ListView_InsertColumn(hWndFiles, 0, &lvc);
+
+ int nIndex = 0;
+
+ iter_each_(ConflictingFiles, fi, *pFiles)
+ {
+ LVITEM lvi;
+
+ lvi.mask = LVIF_TEXT;
+ lvi.iItem = nIndex++;
+ lvi.iSubItem = 0;
+ lvi.pszText = const_cast<mu_text*>(fi->first.c_str());
+
+ ListView_InsertItem(hWndFiles, &lvi);
+ }
+ }
+ else if (uMsg == WM_COMMAND)
+ {
+ switch (LOWORD(wParam))
+ {
+ case IDOK:
+ case IDCANCEL:
+ EndDialog(hDlg, LOWORD(wParam));
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+Statistic::~Statistic()
+{
+ iter_each_(ContactList, i, m_Contacts)
+ {
+ freeContactData(**i);
+ delete *i;
+ }
+
+ m_Contacts.clear();
+
+ if (m_pOmitted)
+ {
+ freeContactData(*m_pOmitted);
+ delete m_pOmitted;
+ }
+
+ if (m_pTotals)
+ {
+ freeContactData(*m_pTotals);
+ delete m_pTotals;
+ }
+}
+
+void Statistic::run(const Settings& settings, InvocationSource invokedFrom, HINSTANCE hInst, HWND hWndParent /* = NULL */)
+{
+ // check if running and make running
+ if (m_bRunning)
+ {
+ MessageBox(
+ 0,
+ i18n(muT("HistoryStats is already generating statistics. Please wait for the already running process to be finished or cancel it and try again.")),
+ i18n(muT("HistoryStats")),
+ MB_ICONINFORMATION | MB_OK);
+ return;
+ }
+
+ m_bRunning = true;
+
+ // create object holding and performing the statistics
+ Statistic* pStats = new Statistic(settings, invokedFrom, hInst);
+
+ // create event for thread stack unwinding
+ if ((pStats->m_hThreadPushEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL)
+ {
+ m_bRunning = false;
+ return;
+ }
+
+ // create worker thread
+ DWORD dwThreadID = 0;
+ HANDLE hThread = CreateThread(NULL, 0, threadProc, pStats, 0, &dwThreadID);
+
+ // wait for thread to place itself on thread unwind stack
+ if (hThread != NULL)
+ {
+ WaitForSingleObject(pStats->m_hThreadPushEvent, INFINITE);
+ }
+ else
+ {
+ m_bRunning = false;
+ }
+
+ CloseHandle(pStats->m_hThreadPushEvent);
+ pStats->m_hThreadPushEvent = NULL;
+}
diff --git a/plugins/HistoryStats/src/statistic.h b/plugins/HistoryStats/src/statistic.h
new file mode 100644
index 0000000000..ee312619be
--- /dev/null
+++ b/plugins/HistoryStats/src/statistic.h
@@ -0,0 +1,210 @@
+#if !defined(HISTORYSTATS_GUARD_STATISTIC_H)
+#define HISTORYSTATS_GUARD_STATISTIC_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include <vector>
+#include <map>
+#include <set>
+#include <list>
+
+#include "settings.h"
+#include "protocol.h"
+#include "message.h"
+
+class Contact; // forward declaration instead of #include "contact.h"
+
+class Statistic
+ : private pattern::NotCopyable<Statistic>
+{
+public:
+ enum InvocationSource {
+ fromOptions,
+ fromStartup,
+ fromMenu,
+ };
+
+ enum DBEventTypeRaw {
+ ICQEVENTTYPE_AUTH_GRANTED = 2004, // from ICQ
+ ICQEVENTTYPE_AUTH_DENIED = 2005, // from ICQ
+ ICQEVENTTYPE_BROADCAST = 2006, // from ICQ
+ ICQEVENTTYPE_SELF_REMOVE = 2007, // from ICQ
+ ICQEVENTTYPE_FUTURE_AUTH = 2008, // from ICQ
+ EVENTTYPE_SMTPSIMPLE = 2350, // from SMTP Simple
+ EVENTTYPE_VOICE_CALL = 8739, // from pescuma
+ EVENTTYPE_NICKNAMECHANGE = 9001, // from pescuma
+ EVENTTYPE_STATUSMESSAGECHANGE = 9002, // from pescuma
+ EVENTTYPE_AVATARCHANGE = 9003, // from pescuma
+ EVENTTYPE_CONTACTLEFTCHANNEL = 9004, // from pescuma
+ EVENTTYPE_WAT_REQUEST = 9601, // from WATrack
+ EVENTTYPE_WAT_ANSWER = 9602, // from WATrack
+ EVENTTYPE_WAT_ERROR = 9603, // from WATrack
+ EVENTTYPE_STATUSCHANGE = 25368, // from SRMMs
+ };
+
+ enum DBEventType {
+ // messages
+ etMessage = EVENTTYPE_MESSAGE,
+ // other events
+ etFile = EVENTTYPE_FILE,
+ etURL = EVENTTYPE_URL,
+ etICQSMS = ICQEVENTTYPE_SMS,
+ etICQWebPager = ICQEVENTTYPE_WEBPAGER,
+ etICQEMailExpress = ICQEVENTTYPE_EMAILEXPRESS,
+ etSMTPSimple = EVENTTYPE_SMTPSIMPLE,
+ etICQBroadcast = ICQEVENTTYPE_BROADCAST,
+ etVoiceCall = EVENTTYPE_VOICE_CALL,
+ // authorization and contacts
+ etAuthRequest = EVENTTYPE_AUTHREQUEST,
+ etAdded = EVENTTYPE_ADDED,
+ etContacts = EVENTTYPE_CONTACTS,
+ etICQAuthGranted = ICQEVENTTYPE_AUTH_GRANTED,
+ etICQAuthDenied = ICQEVENTTYPE_AUTH_DENIED,
+ etICQSelfRemove = ICQEVENTTYPE_SELF_REMOVE,
+ etICQFutureAuth = ICQEVENTTYPE_FUTURE_AUTH,
+ // status/avatar/nick/... changes
+ etStatusChange = EVENTTYPE_STATUSCHANGE,
+ etNickNameChange = EVENTTYPE_NICKNAMECHANGE,
+ etStatusMessageChange = EVENTTYPE_STATUSMESSAGECHANGE,
+ etAvatarChange = EVENTTYPE_AVATARCHANGE,
+ etContactLeftChannel = EVENTTYPE_CONTACTLEFTCHANNEL,
+ // WaTrack events
+ etWATRequest = EVENTTYPE_WAT_REQUEST,
+ etWATAnswer = EVENTTYPE_WAT_ANSWER,
+ etWATError = EVENTTYPE_WAT_ERROR,
+ };
+
+ typedef std::vector<Contact*> ContactList;
+ typedef std::vector<const Contact*> ContactListC;
+ typedef std::pair<ext::string, ext::string> ConflictingFile; // (desired, temp)
+ typedef std::list<ConflictingFile> ConflictingFiles;
+ typedef std::map<Canvas::Digest, ext::string> ImageMap;
+
+private:
+ static bool m_bRunning;
+
+private:
+ // settings and the like
+ Settings m_Settings;
+ Settings::CharMapper m_CharMapper;
+
+ // handles for windows and synchronisation
+ HINSTANCE m_hInst;
+ HWND m_hWndProgress;
+ HANDLE m_hThreadPushEvent;
+ HANDLE m_hCancelEvent;
+ InvocationSource m_InvokedFrom;
+
+ // list of contacts
+ ContactList m_Contacts;
+
+ // special 'contacts': omitted, totals
+ Contact* m_pTotals;
+ Contact* m_pOmitted;
+
+ // did we really omit something
+ bool m_bActuallyOmitted;
+
+ // start time for statistics
+ DWORD m_TimeStarted;
+ DWORD m_MSecStarted;
+
+ // minimum/maximum date/time to include
+ DWORD m_TimeMin;
+ DWORD m_TimeMax;
+
+ // error processing and the like
+ ext::string m_ErrorText;
+
+ // column management
+ int m_nNextSlot;
+ std::vector<Column*> m_ActiveCols;
+ std::vector<Column*> m_AcquireCols;
+ std::vector<Column*> m_TransformCols;
+
+ // file management
+ ext::string m_TempPath;
+ ext::string m_OutputPath;
+ ext::string m_OutputFile;
+ ext::string m_OutputPrefix;
+ ConflictingFiles m_ConflictingFiles;
+ int m_nLastFileNr;
+
+ // PNG management
+ ImageMap m_Images;
+
+ // first/last message
+ bool m_bHistoryTimeAvailable;
+ DWORD m_nFirstTime;
+ DWORD m_nLastTime;
+
+ // misc data
+ DWORD m_AverageMinTime;
+
+private:
+ // contact handling
+ void prepareColumns();
+ void prepareContactData(Contact& contact);
+ void freeContactData(Contact& contact);
+ void mergeContactData(Contact& contact, const Contact& include);
+ void transformContactData(Contact& contact);
+ Contact& addContact(const ext::string& nick, const ext::string& protoDisplayName, const ext::string& groupName, int nSources);
+
+ // misc routines
+ DWORD getTimeStarted() { return m_TimeStarted; }
+ bool shouldTerminate() { return (WaitForSingleObject(m_hCancelEvent, 0) == WAIT_OBJECT_0) || (bool_(mu::system::terminated())); }
+ void handleAddMessage(Contact& contact, Message& msg);
+ void handleAddChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration);
+
+ // progress dialog handling
+ static INT_PTR CALLBACK staticProgressProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
+ void setProgressMax(bool bSub, int max);
+ void setProgressLabel(bool bSub, const ext::string& label);
+ void stepProgress(bool bSub, int step = 1);
+ static INT_PTR CALLBACK staticConflictProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+ // statistics creation steps
+ bool stepInit();
+ bool stepReadDB();
+ bool stepRemoveContacts();
+ bool stepSortContacts();
+ bool stepPreOmitContacts();
+ bool stepOmitContacts();
+ bool stepCalcTotals();
+ bool stepPostOmitContacts();
+ bool stepTransformData();
+ bool stepWriteHTML();
+
+ // private constructor & main statistic creation routine
+ explicit Statistic(const Settings& settings, InvocationSource invokedFrom, HINSTANCE hInst);
+ bool createStatistics();
+ bool createStatisticsSteps();
+ static DWORD WINAPI threadProc(LPVOID lpParameter);
+ static DWORD WINAPI threadProcSteps(LPVOID lpParameter);
+
+public:
+ ~Statistic();
+
+public:
+ static void run(const Settings& settings, InvocationSource invokedFrom, HINSTANCE hInst, HWND hWndParent = NULL);
+
+ int countContacts() const { return m_Contacts.size(); }
+ const Contact& getContact(int index) const;
+ const Contact& getTotals() const { assert(m_pTotals); return *m_pTotals; }
+ const Contact& getOmitted() const { assert(m_pOmitted); return *m_pOmitted; }
+ bool hasTotals() const { return (m_pTotals != NULL) && m_Settings.m_CalcTotals; } // MEMO: only makes sense after 'calc totals'-step
+ bool hasOmitted() const { return (m_pOmitted != NULL) && m_Settings.m_OmitContacts && m_Settings.m_OmittedInExtraRow && m_bActuallyOmitted; } // MEMO: only makes sense after 'omit'-step
+ DWORD getFirstTime(); // MEMO: only makes sense after 'calc totals'-step
+ DWORD getLastTime(); // MEMO: only makes sense after 'calc totals'-step
+ DWORD getHistoryTime(); // MEMO: only makes sense after 'calc totals'-step
+ DWORD getAverageMinTime() { return m_AverageMinTime; }
+
+ // file management
+ ext::string createFile(const ext::string& desiredName);
+ bool newFile(const mu_text* fileExt, ext::string& writeFile, ext::string& finalURL);
+ bool newFilePNG(ext::string& writeFile, ext::string& finalURL) { return newFile(muT(".png"), writeFile, finalURL); }
+ bool newFilePNG(Canvas& canvas, ext::string& finalURL);
+};
+
+#endif // HISTORYSTATS_GUARD_STATISTIC_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/themeapi.cpp b/plugins/HistoryStats/src/themeapi.cpp
new file mode 100644
index 0000000000..a15067bc66
--- /dev/null
+++ b/plugins/HistoryStats/src/themeapi.cpp
@@ -0,0 +1,53 @@
+#include "_globals.h"
+#include "themeapi.h"
+
+/*
+ * ThemeAPI
+ */
+
+const mu_text* ThemeAPI::m_szThemesDll = muT("uxtheme.dll");
+HMODULE ThemeAPI::m_hThemesDll = NULL;
+bool ThemeAPI::m_bUseTheme = false;
+
+ThemeAPI::fnCloseThemeData ThemeAPI::CloseThemeData = NULL;
+ThemeAPI::fnDrawThemeBackground ThemeAPI::DrawThemeBackground = NULL;
+ThemeAPI::fnIsAppThemed ThemeAPI::IsAppThemed = NULL;
+ThemeAPI::fnIsThemeActive ThemeAPI::IsThemeActive = NULL;
+ThemeAPI::fnOpenThemeData ThemeAPI::OpenThemeData = NULL;
+#if !defined(HISTORYSTATS_THEMEAPI_MINIMAL)
+ThemeAPI::fnDrawThemeIcon ThemeAPI::DrawThemeIcon = NULL;
+ThemeAPI::fnEnableThemeDialogTexture ThemeAPI::EnableThemeDialogTexture = NULL;
+ThemeAPI::fnGetThemePartSize ThemeAPI::GetThemePartSize = NULL;
+#endif
+
+void ThemeAPI::init()
+{
+ m_hThemesDll = LoadLibrary(m_szThemesDll);
+
+ if (m_hThemesDll)
+ {
+ CloseThemeData = reinterpret_cast<fnCloseThemeData >(GetProcAddress(m_hThemesDll, muA("CloseThemeData") ));
+ DrawThemeBackground = reinterpret_cast<fnDrawThemeBackground >(GetProcAddress(m_hThemesDll, muA("DrawThemeBackground") ));
+ IsAppThemed = reinterpret_cast<fnIsAppThemed >(GetProcAddress(m_hThemesDll, muA("IsAppThemed") ));
+ IsThemeActive = reinterpret_cast<fnIsThemeActive >(GetProcAddress(m_hThemesDll, muA("IsThemeActive") ));
+ OpenThemeData = reinterpret_cast<fnOpenThemeData >(GetProcAddress(m_hThemesDll, muA("OpenThemeData") ));
+#if !defined(HISTORYSTATS_THEMEAPI_MINIMAL)
+ DrawThemeIcon = reinterpret_cast<fnDrawThemeIcon >(GetProcAddress(m_hThemesDll, muA("DrawThemeIcon") ));
+ EnableThemeDialogTexture = reinterpret_cast<fnEnableThemeDialogTexture>(GetProcAddress(m_hThemesDll, muA("EnableThemeDialogTexture")));
+ GetThemePartSize = reinterpret_cast<fnGetThemePartSize >(GetProcAddress(m_hThemesDll, muA("GetThemePartSize") ));
+#endif
+
+ m_bUseTheme = IsThemeActive && IsAppThemed;
+ }
+}
+
+void ThemeAPI::uninit()
+{
+ if (m_hThemesDll)
+ {
+ FreeLibrary(m_hThemesDll);
+
+ m_hThemesDll = NULL;
+ m_bUseTheme = false;
+ }
+} \ No newline at end of file
diff --git a/plugins/HistoryStats/src/themeapi.h b/plugins/HistoryStats/src/themeapi.h
new file mode 100644
index 0000000000..fe3444a604
--- /dev/null
+++ b/plugins/HistoryStats/src/themeapi.h
@@ -0,0 +1,50 @@
+#if !defined(HISTORYSTATS_GUARD_THEMEAPI_H)
+#define HISTORYSTATS_GUARD_THEMEAPI_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+#include <uxtheme.h>
+#include <tmschema.h>
+
+#define HISTORYSTATS_THEMEAPI_MINIMAL
+
+class ThemeAPI
+ : private pattern::NotInstantiable<ThemeAPI>
+{
+public:
+ typedef HRESULT (WINAPI *fnCloseThemeData )(HTHEME hTheme);
+ typedef HRESULT (WINAPI *fnDrawThemeBackground )(HTHEME hTheme, HDC hdc, int iPartId, int iStateId, const RECT *pRect, const RECT *pClipRect);
+ typedef BOOL (WINAPI *fnIsAppThemed )(VOID);
+ typedef BOOL (WINAPI *fnIsThemeActive )(VOID);
+ typedef HTHEME (WINAPI *fnOpenThemeData )(HWND hwnd, LPCWSTR pszClassList);
+#if !defined(HISTORYSTATS_THEMEAPI_MINIMAL)
+ typedef HRESULT (WINAPI *fnDrawThemeIcon )(HTHEME hTheme, HDC hdc, int iPartId, int iStateId, const RECT *pRect, HIMAGELIST himl, int iImageIndex);
+ typedef HRESULT (WINAPI *fnEnableThemeDialogTexture)(HWND hwnd, DWORD dwFlags);
+ typedef HRESULT (WINAPI *fnGetThemePartSize )(HTHEME hTheme, HDC hdc, int iPartId, int iStateId, RECT *prc, THEMESIZE eSize, SIZE *psz);
+#endif
+
+public:
+ static fnCloseThemeData CloseThemeData;
+ static fnDrawThemeBackground DrawThemeBackground;
+ static fnIsAppThemed IsAppThemed;
+ static fnIsThemeActive IsThemeActive;
+ static fnOpenThemeData OpenThemeData;
+#if !defined(HISTORYSTATS_THEMEAPI_MINIMAL)
+ static fnDrawThemeIcon DrawThemeIcon;
+ static fnEnableThemeDialogTexture EnableThemeDialogTexture;
+ static fnGetThemePartSize GetThemePartSize;
+#endif
+
+private:
+ static const mu_text* m_szThemesDll;
+ static HMODULE m_hThemesDll;
+ static bool m_bUseTheme;
+
+public:
+ static void init();
+ static void uninit();
+ static bool useTheme() { return m_bUseTheme && IsThemeActive() && IsAppThemed(); }
+};
+
+#endif // HISTORYSTATS_GUARD_THEMEAPI_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/utf8buffer.h b/plugins/HistoryStats/src/utf8buffer.h
new file mode 100644
index 0000000000..9d1b013d99
--- /dev/null
+++ b/plugins/HistoryStats/src/utf8buffer.h
@@ -0,0 +1,119 @@
+#if !defined(HISTORYSTATS_GUARD_UTF8BUFFER_H)
+#define HISTORYSTATS_GUARD_UTF8BUFFER_H
+
+#include <streambuf>
+#include <string>
+
+class UTF8Buffer
+ : public std::basic_streambuf<mu_text, std::char_traits<mu_text> >
+ , private pattern::NotCopyable<UTF8Buffer>
+{
+private:
+ typedef std::char_traits<mu_text> _Tr;
+
+private:
+ int m_BufSize;
+ mu_text* m_pBuf;
+ ext::a::ostream& m_Stream;
+ mu_ansi* m_pUTF8Buf;
+ mu_wide* m_pBufW;
+
+#if !defined(MU_WIDE)
+ mu_wide m_CharMap[256];
+#endif // MU_WIDE
+
+private:
+ void dump(const mu_text* pBegin, const mu_text* pEnd)
+ {
+ size_t len = pEnd - pBegin;
+
+#if !defined(MU_WIDE)
+ mu_wide* pW = m_pBufW;
+ mu_ansi* pA = m_pBuf;
+
+ while (pA < pEnd)
+ {
+ *pW++ = m_CharMap[static_cast<unsigned char>(*pA++)];
+ }
+#endif // MU_WIDE
+ size_t utf8_len = utils::rawUTF8Encode(m_pBufW, len, m_pUTF8Buf);
+
+ m_Stream.write(m_pUTF8Buf, utf8_len);
+ }
+
+protected:
+ virtual int_type overflow(int_type _C = UTF8Buffer::_Tr::eof())
+ {
+ if (_Tr::eq_int_type(_Tr::eof(), _C))
+ {
+ return _Tr::not_eof(_C);
+ }
+ else if (pptr() != 0 && pptr() < epptr())
+ {
+ *pptr() = _Tr::to_char_type(_C);
+ pbump(1);
+
+ return _C;
+ }
+ else
+ {
+ dump(pbase(), pptr());
+
+ setp(m_pBuf, m_pBuf + m_BufSize);
+
+ *pptr() = _Tr::to_char_type(_C);
+ pbump(1);
+
+ return _C;
+ }
+ }
+
+ virtual int sync()
+ {
+ dump(pbase(), pptr());
+
+ setp(m_pBuf, m_pBuf + m_BufSize);
+
+ return 0;
+ }
+
+public:
+ explicit UTF8Buffer(ext::a::ostream& outStream, int bufferSize = 4096)
+ : m_BufSize(bufferSize), m_Stream(outStream)
+ {
+ m_pBuf = new mu_text[m_BufSize];
+ m_pUTF8Buf = new mu_ansi[3 * m_BufSize];
+
+#if defined(MU_WIDE)
+ m_pBufW = m_pBuf;
+#else // MU_WIDE
+ m_pBufW = new mu_wide[m_BufSize];
+#endif // MU_WIDE
+
+#if !defined(MU_WIDE)
+ mu_ansi ANSIChars[256];
+
+ array_each_(i, ANSIChars)
+ {
+ ANSIChars[i] = static_cast<mu_ansi>(i);
+ }
+
+ m_CharMap[0] = muC(' ');
+ MultiByteToWideChar(CP_ACP, 0, ANSIChars + 1, 255, m_CharMap + 1, 255);
+#endif // MU_WIDE
+
+ setp(m_pBuf, m_pBuf + m_BufSize);
+ }
+
+ virtual ~UTF8Buffer()
+ {
+ delete[] m_pBuf;
+ delete[] m_pUTF8Buf;
+
+#if !defined(MU_WIDE)
+ delete[] m_pBufW;
+#endif // MU_WIDE
+ }
+};
+
+#endif // HISTORYSTATS_GUARD_UTF8BUFFER_H
diff --git a/plugins/HistoryStats/src/utils.cpp b/plugins/HistoryStats/src/utils.cpp
new file mode 100644
index 0000000000..ba1cabc53f
--- /dev/null
+++ b/plugins/HistoryStats/src/utils.cpp
@@ -0,0 +1,1206 @@
+#include "_globals.h"
+#include "utils.h"
+
+#include <ctime>
+#include <clocale>
+#include <cstdio>
+#include <stack>
+
+/*
+ * utils
+ */
+
+namespace utils
+{
+ ext::string timestampToString(DWORD value, const mu_text* format)
+ {
+ mu_text temp[100] = { 0 };
+
+ return (ext::strfunc::ftime(temp, 100, format, gmtime(reinterpret_cast<time_t*>(&value))) > 0) ? temp : muT("");
+ }
+
+ ext::string tmStructToString(const tm& value, const mu_text* format)
+ {
+ mu_text temp[100] = { 0 };
+
+ return (ext::strfunc::ftime(temp, 100, format, &value) > 0) ? temp : muT("");
+ }
+
+ ext::string durationToString(DWORD value)
+ {
+ mu_text temp[100] = { 0 };
+
+ value += 59;
+ value /= 60;
+
+ if (value >= 1440)
+ {
+ ext::strfunc::sprintf(temp, muT("%dd %02d:%02d"), value / 1440, (value / 60) % 24, value % 60);
+ }
+ else
+ {
+ ext::strfunc::sprintf(temp, muT("%02d:%02d"), value / 60, value % 60);
+ }
+
+ return temp;
+ }
+
+ DWORD parseDate(const ext::string& date)
+ {
+ if (date.length() != 10 || date[4] != muC('-') || date[7] != muC('-'))
+ {
+ return 0;
+ }
+
+ struct tm dateTM;
+
+ dateTM.tm_year = _ttoi(date.c_str() + 0) - 1900;
+ dateTM.tm_mon = _ttoi(date.c_str() + 5) - 1;
+ dateTM.tm_mday = _ttoi(date.c_str() + 8);
+ dateTM.tm_hour = dateTM.tm_min = dateTM.tm_sec = 0;
+ dateTM.tm_isdst = dateTM.tm_wday = dateTM.tm_yday = 0;
+
+ time_t dateTT = mktime(&dateTM);
+
+ if (dateTT == -1)
+ {
+ return 0;
+ }
+
+ dateTM.tm_year = 1970 - 1900;
+ dateTM.tm_mon = 1 - 1;
+ dateTM.tm_mday = 3;
+ dateTM.tm_hour = dateTM.tm_min = dateTM.tm_sec = 0;
+ dateTM.tm_isdst = dateTM.tm_wday = dateTM.tm_yday = 0;
+
+ time_t baseTT = mktime(&dateTM);
+
+ if (baseTT == -1)
+ {
+ return 0;
+ }
+
+ return dateTT - baseTT + 2 * 86400;
+ }
+
+ ext::string intToString(int value)
+ {
+ mu_text temp[100] = { 0 };
+
+ ext::strfunc::sprintf(temp, muT("%d"), value);
+
+ return temp;
+ }
+
+ ext::string intToPadded(int value, int len)
+ {
+ mu_text temp[100] = { 0 };
+
+ ext::strfunc::sprintf(temp, muT("%0*d"), len, value);
+
+ return temp;
+ }
+
+ ext::string intToGrouped(int value)
+ {
+ mu_text temp[100] = { 0 };
+ const char* grouping = Locale::grouping();
+
+ ext::strfunc::sprintf(temp, muT("%d"), value);
+
+ if (*grouping == CHAR_MAX || *grouping <= 0)
+ {
+ return temp;
+ }
+
+ ext::string str = temp;
+ ext::string::size_type pos = str.length();
+ ext::string::size_type prefix = (temp[0] == muC('+') || temp[0] == muC('-')) ? 1 : 0;
+
+ while (*grouping != CHAR_MAX && *grouping > 0 && pos > prefix + *grouping)
+ {
+ str.insert(pos -= *grouping, 1, Locale::thousandSep());
+
+ if (grouping[1] > 0)
+ {
+ ++grouping;
+ }
+ }
+
+ return str;
+ }
+
+ ext::string floatToString(double value, int precision)
+ {
+ mu_text temp[100] = { 0 };
+
+ ext::strfunc::sprintf(temp, muT("%.*f"), precision, value);
+
+ return temp;
+ }
+
+ ext::string floatToGrouped(double value, int precision)
+ {
+ mu_text temp[100] = { 0 };
+ const char* grouping = Locale::grouping();
+
+ ext::strfunc::sprintf(temp, muT("%.*f"), precision, value);
+
+ if (*grouping == CHAR_MAX || *grouping <= 0)
+ {
+ return temp;
+ }
+
+ ext::string str = temp;
+ ext::string::size_type pos = str.find(Locale::decimalPoint());
+ ext::string::size_type prefix = (temp[0] == muC('+') || temp[0] == muC('-')) ? 1 : 0;
+
+ if (pos == ext::string::npos)
+ {
+ pos = str.length();
+ }
+
+ while (*grouping != CHAR_MAX && *grouping > 0 && pos > prefix + *grouping)
+ {
+ str.insert(pos -= *grouping, 1, Locale::thousandSep());
+
+ if (grouping[1] > 0)
+ {
+ ++grouping;
+ }
+ }
+
+ return str;
+ }
+
+ ext::string ratioToPercent(int numerator, int denominator)
+ {
+ float value = 0.0;
+ mu_text temp[100] = { 0 };
+
+ if (denominator != 0)
+ {
+ value = 1.0f * numerator / denominator;
+ }
+
+ ext::strfunc::sprintf(temp, muT("%.0f%%"), 100.0f * value);
+
+ return temp;
+ }
+
+ void replaceAllInPlace(ext::string& text, const mu_text* find, const mu_text* replace)
+ {
+ ext::string::size_type pos = 0;
+ ext::string::size_type find_len = ext::strfunc::len(find);
+ ext::string::size_type replace_len = ext::strfunc::len(replace);
+
+ while ((pos = text.find(find, pos, find_len)) != ext::string::npos)
+ {
+ text.erase(pos, find_len);
+ text.insert(pos, replace, replace_len);
+ pos += replace_len;
+ }
+ }
+
+ void htmlEscapeInPlace(ext::string& text)
+ {
+ replaceAllInPlace(text, muT("&") , muT("&amp;") );
+ replaceAllInPlace(text, muT("\""), muT("&quot;"));
+ replaceAllInPlace(text, muT("<") , muT("&lt;") );
+ replaceAllInPlace(text, muT(">") , muT("&gt;") );
+ }
+
+ const mu_text* stripPrefix(const mu_text* szPrefix, const mu_text* szText)
+ {
+ int i = 0;
+
+ while (szPrefix[i] != muC('\0') && szText[i] != muC('\0') && szPrefix[i] == szText[i])
+ {
+ ++i;
+ }
+
+ if (szPrefix[i] == muC('\0'))
+ {
+ return szText + i;
+ }
+ else
+ {
+ return szText;
+ }
+ }
+
+ ext::string replaceVariables(const ext::string& strFormat, DWORD timeValue, const mu_text* szNick /* = muT("") */)
+ {
+ static const mu_text* szMonthName[][2] = {
+ { I18N(muT("month3:Jan")), I18N(muT("monthF:January")) },
+ { I18N(muT("month3:Feb")), I18N(muT("monthF:February")) },
+ { I18N(muT("month3:Mar")), I18N(muT("monthF:March")) },
+ { I18N(muT("month3:Apr")), I18N(muT("monthF:April")) },
+ { I18N(muT("month3:May")), I18N(muT("monthF:May")) },
+ { I18N(muT("month3:Jun")), I18N(muT("monthF:June")) },
+ { I18N(muT("month3:Jul")), I18N(muT("monthF:July")) },
+ { I18N(muT("month3:Aug")), I18N(muT("monthF:August")) },
+ { I18N(muT("month3:Sep")), I18N(muT("monthF:September")) },
+ { I18N(muT("month3:Oct")), I18N(muT("monthF:October")) },
+ { I18N(muT("month3:Nov")), I18N(muT("monthF:November")) },
+ { I18N(muT("month3:Dec")), I18N(muT("monthF:December")) },
+ };
+
+ static const mu_text* szWDayName[][3] = {
+ { I18N(muT("wday2:Mo")), I18N(muT("wday3:Mon")), I18N(muT("wdayF:Monday")) },
+ { I18N(muT("wday2:Tu")), I18N(muT("wday3:Tue")), I18N(muT("wdayF:Tuesday")) },
+ { I18N(muT("wday2:We")), I18N(muT("wday3:Wed")), I18N(muT("wdayF:Wednesday")) },
+ { I18N(muT("wday2:Th")), I18N(muT("wday3:Thu")), I18N(muT("wdayF:Thursday")) },
+ { I18N(muT("wday2:Fr")), I18N(muT("wday3:Fri")), I18N(muT("wdayF:Friday")) },
+ { I18N(muT("wday2:Sa")), I18N(muT("wday3:Sat")), I18N(muT("wdayF:Saturday")) },
+ { I18N(muT("wday2:Su")), I18N(muT("wday3:Sun")), I18N(muT("wdayF:Sunday")) },
+ };
+
+ struct tm timeTM = *gmtime(reinterpret_cast<time_t*>(&timeValue));
+
+ ext::string strOut = strFormat;
+ ext::string::size_type posOpen = strOut.find(muC('%'));
+
+ while (posOpen != ext::string::npos)
+ {
+ ext::string::size_type posClose = strOut.find(muC('%'), posOpen + 1);
+
+ if (posOpen != ext::string::npos)
+ {
+ ext::string strVar = strOut.substr(posOpen + 1, posClose - posOpen - 1);
+ ext::string strSubst;
+
+ // match variable and generate substitution
+ if (strVar == muT("h"))
+ {
+ strSubst = intToString(timeTM.tm_hour % 12 + (timeTM.tm_hour % 12 == 0 ? 12 : 0));
+ }
+ else if (strVar == muT("hh"))
+ {
+ strSubst = intToPadded(timeTM.tm_hour % 12 + (timeTM.tm_hour % 12 == 0 ? 12 : 0), 2);
+ }
+ else if (strVar == muT("H"))
+ {
+ strSubst = intToString(timeTM.tm_hour);
+ }
+ else if (strVar == muT("HH"))
+ {
+ strSubst = intToPadded(timeTM.tm_hour, 2);
+ }
+ else if (strVar == muT("m"))
+ {
+ strSubst = intToString(timeTM.tm_min);
+ }
+ else if (strVar == muT("mm"))
+ {
+ strSubst = intToPadded(timeTM.tm_min, 2);
+ }
+ else if (strVar == muT("s"))
+ {
+ strSubst = intToString(timeTM.tm_sec);
+ }
+ else if (strVar == muT("ss"))
+ {
+ strSubst = intToPadded(timeTM.tm_sec, 2);
+ }
+ else if (strVar == muT("tt"))
+ {
+ strSubst = timeTM.tm_hour / 12 ? i18n(muT("pm")) : i18n(muT("am"));
+ }
+ else if (strVar == muT("TT"))
+ {
+ strSubst = timeTM.tm_hour / 12 ? i18n(muT("PM")) : i18n(muT("AM"));
+ }
+ else if (strVar == muT("yy"))
+ {
+ strSubst = intToPadded((timeTM.tm_year + 1900) % 100, 2);
+ }
+ else if (strVar == muT("yyyy"))
+ {
+ strSubst = intToPadded(timeTM.tm_year + 1900, 4);
+ }
+ else if (strVar == muT("M"))
+ {
+ strSubst = intToString(timeTM.tm_mon + 1);
+ }
+ else if (strVar == muT("MM"))
+ {
+ strSubst = intToPadded(timeTM.tm_mon + 1, 2);
+ }
+ else if (strVar == muT("MMM"))
+ {
+ strSubst = stripPrefix(muT("month3:"), i18n(szMonthName[timeTM.tm_mon % 12][0]));
+ }
+ else if (strVar == muT("MMMM"))
+ {
+ strSubst = stripPrefix(muT("monthF:"), i18n(szMonthName[timeTM.tm_mon % 12][1]));
+ }
+ else if (strVar == muT("d"))
+ {
+ strSubst = intToString(timeTM.tm_mday);
+ }
+ else if (strVar == muT("dd"))
+ {
+ strSubst = intToPadded(timeTM.tm_mday, 2);
+ }
+ else if (strVar == muT("ww"))
+ {
+ strSubst = stripPrefix(muT("wday2:"), i18n(szWDayName[(timeTM.tm_wday + 6) % 7][0]));
+ }
+ else if (strVar == muT("www"))
+ {
+ strSubst = stripPrefix(muT("wday3:"), i18n(szWDayName[(timeTM.tm_wday + 6) % 7][1]));
+ }
+ else if (strVar == muT("wwww"))
+ {
+ strSubst = stripPrefix(muT("wdayF:"), i18n(szWDayName[(timeTM.tm_wday + 6) % 7][2]));
+ }
+ else if (strVar == muT("miranda_path"))
+ {
+ strSubst = getMirandaPath();
+ }
+ else if (strVar == muT("profile_path"))
+ {
+ strSubst = getProfilePath();
+ }
+ else if (strVar == muT("profile_name"))
+ {
+ strSubst = getProfileName();
+ }
+ else if (strVar == muT("nick"))
+ {
+ strSubst = szNick;
+ }
+ else if (strVar == muT(""))
+ {
+ strSubst = muT("%");
+ }
+
+ // perform actual substitution
+ if (!strSubst.empty())
+ {
+ strOut.replace(posOpen, posClose - posOpen + 1, strSubst);
+ posClose += strSubst.length() - strVar.length() - 2;
+ }
+ }
+ else
+ {
+ break;
+ }
+
+ posOpen = strOut.find(muC('%'), posClose + 1);
+ }
+
+ return strOut;
+ }
+
+ ext::string toLowerCase(const ext::string& text)
+ {
+ int len = text.length();
+ mu_text* buf = new mu_text[len + 1];
+
+ LCID lcid = GetUserDefaultLCID();
+
+ len = LCMapString(lcid, LCMAP_LINGUISTIC_CASING | LCMAP_LOWERCASE, text.c_str(), len, buf, len);
+
+ buf[len] = 0;
+
+ ext::string ret_str(buf, len);
+
+ delete[] buf;
+
+ return ret_str;
+ }
+
+ ext::string toUpperCase(const ext::string& text)
+ {
+ int len = text.length();
+ mu_text* buf = new mu_text[len + 1];
+
+ LCID lcid = GetUserDefaultLCID();
+
+ len = LCMapString(lcid, LCMAP_LINGUISTIC_CASING | LCMAP_UPPERCASE, text.c_str(), len, buf, len);
+
+ buf[len] = 0;
+
+ ext::string ret_str(buf, len);
+
+ delete[] buf;
+
+ return ret_str;
+ }
+
+ DWORD dottedToVersion(ext::string version)
+ {
+ union {
+ __int32 combined;
+ __int8 parts[4];
+ } res = { 0 };
+
+ int part = 3;
+
+ while (!version.empty() && part >= 0)
+ {
+ ext::string::size_type dotPos = version.find(muT("."));
+
+ if (dotPos == ext::string::npos)
+ {
+ dotPos = version.length();
+ }
+
+ res.parts[part--] = _ttoi(version.substr(0, dotPos).c_str());
+
+ version.erase(0, dotPos + 1);
+ }
+
+ return res.combined;
+ }
+
+ ext::string versionToDotted(DWORD version)
+ {
+ mu_text temp[16] = { 0 };
+
+ ext::strfunc::sprintf(
+ temp,
+ muT("%d.%d.%d.%d"),
+ (version >> 24) & 0xFF,
+ (version >> 16) & 0xFF,
+ (version >> 8) & 0xFF,
+ version & 0xFF);
+
+ return temp;
+ }
+
+ ext::a::string convertWToA(const mu_wide* str, size_t len)
+ {
+ mu_ansi* buf = new mu_ansi[len + 1];
+
+ len = WideCharToMultiByte(CP_ACP, 0, str, len, buf, len, NULL, NULL);
+
+ buf[len] = muC('\0');
+
+ ext::a::string ret_str(buf, len);
+
+ delete[] buf;
+
+ return ret_str;
+ }
+
+ ext::w::string convertAToW(const mu_ansi* str, size_t len)
+ {
+ mu_wide* buf = new mu_wide[len + 1];
+
+ len = MultiByteToWideChar(CP_ACP, 0, str, len, buf, len);
+
+ buf[len] = muC('\0');
+
+ ext::w::string ret_str(buf, len);
+
+ delete[] buf;
+
+ return ret_str;
+ }
+
+ ext::a::string convertTToUTF8(const mu_text* str, size_t str_len)
+ {
+#if defined(MU_WIDE)
+ const mu_wide* conv_str = str;
+#else // MU_WIDE
+ const ext::w::string conv_strX = convertAToW(str, str_len);
+ const mu_wide* conv_str = conv_strX.c_str();
+#endif // MU_WIDE
+
+ int len = 0;
+
+ upto_each_(i, str_len)
+ {
+ mu_wide c = conv_str[i];
+
+ if (c <= 0x007F)
+ {
+ len++;
+ }
+ else if (c <= 0x07FF)
+ {
+ len += 2;
+ }
+ else
+ {
+ len += 3;
+ }
+ }
+
+ ext::a::string out_str(len, muC('_'));
+
+ int pos = 0;
+
+ upto_each_(i, str_len)
+ {
+ mu_wide c = conv_str[i];
+
+ if (c <= 0x007F)
+ {
+ out_str[pos++] = (unsigned char) c;
+ }
+ else if (c <= 0x07FF)
+ {
+ out_str[pos++] = (unsigned char) 0xC0 | (c >> 6);
+ out_str[pos++] = (unsigned char) 0x80 | (c & 0x3F);
+ }
+ else
+ {
+ out_str[pos++] = (unsigned char) 0xE0 | (c >> 12);
+ out_str[pos++] = (unsigned char) 0x80 | ((c >> 6) & 0x3F);
+ out_str[pos++] = (unsigned char) 0x80 | (c & 0x3F);
+ }
+ }
+
+ return out_str;
+ }
+
+ ext::string convertUTF8ToT(const mu_ansi* str, size_t str_len)
+ {
+ size_t len = 0, in_pos = 0;
+
+ while (in_pos < str_len)
+ {
+ mu_ansi c = str[in_pos];
+
+ if ((c & 0x80) == 0x00)
+ {
+ in_pos++;
+ }
+ else if ((c & 0xE0) == 0xC0)
+ {
+ in_pos += 2;
+ }
+ else if ((c & 0xF0) == 0xE0)
+ {
+ in_pos += 3;
+ }
+ else
+ {
+ in_pos++;
+ }
+
+ len++;
+ }
+
+ ext::w::string out_str(len, muC('_'));
+
+ size_t out_pos = 0;
+ in_pos = 0;
+
+ while (in_pos < str_len)
+ {
+ unsigned char c = (unsigned char) str[in_pos];
+
+ if ((c & 0x80) == 0x00)
+ {
+ out_str[out_pos] = (mu_wide) c;
+ in_pos++;
+ }
+ else if ((c & 0xE0) == 0xC0)
+ {
+ out_str[out_pos] = (mu_wide) (((c & 0x1F) << 6) | ((unsigned char) str[in_pos + 1] & 0x3F));
+ in_pos += 2;
+ }
+ else if ((c & 0xF0) == 0xE0)
+ {
+ out_str[out_pos] = (mu_wide) (((c & 0x0F) << 12) | (((unsigned char) str[in_pos + 1] & 0x3F) << 6) | ((unsigned char) str[in_pos + 2] & 0x3F));
+ in_pos += 3;
+ }
+ else
+ {
+ in_pos++;
+ }
+
+ out_pos++;
+ }
+
+#if defined(MU_WIDE)
+ return out_str;
+#else // MU_WIDE
+ return convertWToA(out_str.c_str(), out_str.length());
+#endif // MU_WIDE
+ }
+
+ size_t rawUTF8Encode(const mu_wide* pIn, size_t lenIn, mu_ansi* pOut)
+ {
+ mu_ansi* pOutBegin = pOut;
+
+ upto_each_(i, lenIn)
+ {
+ mu_wide c = pIn[i];
+
+ if (c <= 0x007F)
+ {
+ *pOut++ = (unsigned char) c;
+ }
+ else if (c <= 0x07FF)
+ {
+ *pOut++ = (unsigned char) 0xC0 | (c >> 6);
+ *pOut++ = (unsigned char) 0x80 | (c & 0x3F);
+ }
+ else
+ {
+ *pOut++ = (unsigned char) 0xE0 | (c >> 12);
+ *pOut++ = (unsigned char) 0x80 | ((c >> 6) & 0x3F);
+ *pOut++ = (unsigned char) 0x80 | (c & 0x3F);
+ }
+ }
+
+ return (pOut - pOutBegin);
+ }
+
+ size_t getUTF8Len(const mu_ansi* str)
+ {
+ size_t len = 0, in_pos = 0, str_len = ext::a::strfunc::len(str);
+
+ while (in_pos < str_len)
+ {
+ mu_ansi c = str[in_pos];
+
+ if ((c & 0x80) == 0x00)
+ {
+ in_pos++;
+ }
+ else if ((c & 0xE0) == 0xC0)
+ {
+ in_pos += 2;
+ }
+ else if ((c & 0xF0) == 0xE0)
+ {
+ in_pos += 3;
+ }
+ else
+ {
+ in_pos++;
+ }
+
+ len++;
+ }
+
+ return len;
+ }
+
+ bool fileExists(const ext::string& fileName)
+ {
+ WIN32_FIND_DATA wfd;
+ HANDLE hFind = FindFirstFile(fileName.c_str(), &wfd);
+
+ if (hFind != INVALID_HANDLE_VALUE)
+ {
+ FindClose(hFind);
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ bool pathExists(const ext::string& path)
+ {
+ WIN32_FIND_DATA wfd;
+ HANDLE hFind = FindFirstFile((path + muT(".")).c_str(), &wfd);
+
+ if (hFind != INVALID_HANDLE_VALUE)
+ {
+ FindClose(hFind);
+
+ return (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ bool isRelative(const ext::string& fileName)
+ {
+ if (fileName.length() > 2)
+ {
+ if ((fileName[1] == muC(':') && fileName[2] == muC('\\')) || (fileName[0] == muC('\\') && fileName[1] == muC('\\')))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool isValidFilePart(ext::string filePart)
+ {
+ // check for disallowed chars
+ if (filePart.find_first_of(muT("<>:\"/\\|?*")) != ext::string::npos)
+ {
+ return false;
+ }
+
+ // check for dots only
+ if (filePart.find_first_not_of(muC('.')) == ext::string::npos)
+ {
+ return false;
+ }
+
+ // check for disallowed names
+ static const mu_text* disallowedNames[] = {
+ muT("clock$"),
+ muT("aux"),
+ muT("con"),
+ muT("nul"),
+ muT("prn"),
+ muT("com1"),
+ muT("com2"),
+ muT("com3"),
+ muT("com4"),
+ muT("com5"),
+ muT("com6"),
+ muT("com7"),
+ muT("com8"),
+ muT("com9"),
+ muT("lpt1"),
+ muT("lpt2"),
+ muT("lpt3"),
+ muT("lpt4"),
+ muT("lpt5"),
+ muT("lpt6"),
+ muT("lpt7"),
+ muT("lpt8"),
+ muT("lpt9")
+ };
+
+ ext::string::size_type pos = filePart.find(muC('.'));
+
+ if (pos != ext::string::npos)
+ {
+ filePart.erase(pos);
+ }
+
+ array_each_(i, disallowedNames)
+ {
+ if (filePart == disallowedNames[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool isValidFile(const ext::string& fileName)
+ {
+ // find the last backslash to extract file name
+ ext::string::size_type pos = fileName.rfind(muC('\\'));
+
+ if (pos == ext::string::npos)
+ {
+ pos = 0;
+ }
+ else
+ {
+ // is a path, if ends with a backslash
+ if (pos == fileName.length() - 1)
+ return false;
+
+ ++pos;
+ }
+
+ // extract file part
+ return isValidFilePart(fileName.substr(pos));
+ }
+
+ ext::string extractPath(const ext::string& fileName)
+ {
+ ext::string::size_type pos = fileName.rfind(muC('\\'));
+
+ if (pos == ext::string::npos)
+ {
+ return muT("");
+ }
+ else
+ {
+ return fileName.substr(0, pos + 1);
+ }
+ }
+
+ ext::string extractFile(const ext::string& fileName)
+ {
+ ext::string::size_type pos = fileName.rfind(muC('\\'));
+
+ if (pos == ext::string::npos)
+ {
+ return fileName;
+ }
+ else
+ {
+ return fileName.substr(pos + 1);
+ }
+ }
+
+ bool createPath(const ext::string& path)
+ {
+ ext::string curPath = extractPath(path);
+ std::stack<ext::string> subDirs;
+
+ // create stack of missing subdirs and validate them
+ while (curPath.length() > 3 && !pathExists(curPath))
+ {
+ ext::string::size_type pos = curPath.rfind(muC('\\'), curPath.length() - 2);
+
+ if (pos == ext::string::npos)
+ {
+ pos = -1;
+ }
+
+ subDirs.push(curPath.substr(pos + 1, curPath.length() - pos - 2));
+ curPath.erase(pos + 1);
+
+ if (!isValidFilePart(subDirs.top()))
+ {
+ return false;
+ }
+ }
+
+ // try to create subdirs in reverse order
+ while (!subDirs.empty())
+ {
+ const ext::string& curDir = subDirs.top();
+
+ curPath += curDir;
+
+ if (!CreateDirectory(curPath.c_str(), NULL))
+ {
+ return false;
+ }
+
+ curPath += muT("\\");
+
+ subDirs.pop();
+ }
+
+ return true;
+ }
+
+ ext::string colorToHTML(COLORREF crColor)
+ {
+ static const mu_text hexDigits[] = muT("0123456789ABCDEF");
+
+ ext::string htmlColor(7, muC('#'));
+
+ upto_each_(i, 3)
+ {
+ htmlColor[2 * i + 1] = hexDigits[(crColor >> 4) & 0xF];
+ htmlColor[2 * i + 2] = hexDigits[crColor & 0xF];
+
+ crColor >>= 8;
+ }
+
+ return htmlColor;
+ }
+
+ void generateGradient(COLORREF fromColor, COLORREF toColor, COLORREF colorTab[256])
+ {
+ struct rgb { int r, g, b; };
+
+ rgb fromRGB = { GetRValue(fromColor), GetGValue(fromColor), GetBValue(fromColor) };
+ rgb toRGB = { GetRValue(toColor), GetGValue(toColor), GetBValue(toColor) };
+
+ upto_each_(i, 256)
+ {
+ colorTab[i] = RGB(
+ (toRGB.r * i + fromRGB.r * (255 - i)) / 255,
+ (toRGB.g * i + fromRGB.g * (255 - i)) / 255,
+ (toRGB.b * i + fromRGB.b * (255 - i)) / 255);
+ }
+ }
+
+ void ensureRange(int& value, int min, int max, int fallback)
+ {
+ if (value < min || value > max)
+ {
+ value = fallback;
+ }
+ }
+
+ void ensureRange(unsigned int& value, unsigned int min, unsigned int max, unsigned int fallback)
+ {
+ if (value < min || value > max)
+ {
+ value = fallback;
+ }
+ }
+
+ ext::string getGUID()
+ {
+ static const mu_text hexDigits[] = muT("0123456789ABCDEF");
+ GUID guid;
+
+ CoCreateGuid(&guid);
+
+ ext::string strGUID(2 * sizeof(guid), muC('_'));
+
+ upto_each_(i, sizeof(guid))
+ {
+ BYTE val = reinterpret_cast<BYTE*>(&guid)[i];
+
+ strGUID[2 * i] = hexDigits[(val >> 4) & 0xF];
+ strGUID[2 * i + 1] = hexDigits[val & 0xF];
+ }
+
+ return strGUID;
+ }
+
+ void centerDialog(HWND hDlg, HWND hParent /* = NULL */)
+ {
+ if (!hParent)
+ {
+ hParent = GetParent(hDlg);
+ }
+
+ RECT rDlg, rParent;
+
+ if (GetWindowRect(hParent, &rParent) && GetWindowRect(hDlg, &rDlg))
+ {
+ SetWindowPos(
+ hDlg,
+ 0,
+ (rParent.right + rParent.left - rDlg.right + rDlg.left) / 2,
+ (rParent.bottom + rParent.top - rDlg.bottom + rDlg.top) / 2,
+ 0,
+ 0,
+ SWP_NOSIZE | SWP_NOZORDER);
+ }
+ else if (GetWindowRect(hDlg, &rDlg))
+ {
+ SetWindowPos(
+ hDlg,
+ 0,
+ (GetSystemMetrics(SM_CXSCREEN) - rDlg.right + rDlg.left) / 2,
+ (GetSystemMetrics(SM_CYSCREEN) - rDlg.bottom + rDlg.top) / 2,
+ 0,
+ 0,
+ SWP_NOSIZE | SWP_NOZORDER);
+ }
+ }
+
+ RECT getWindowRect(HWND hParent, HWND hWnd)
+ {
+ RECT rWnd;
+
+ GetWindowRect(hWnd, &rWnd);
+
+ ScreenToClient(hParent, reinterpret_cast<POINT*>(&rWnd) + 0);
+ ScreenToClient(hParent, reinterpret_cast<POINT*>(&rWnd) + 1);
+
+ return rWnd;
+ }
+
+ void moveWindow(HWND hWnd, const RECT& rWnd)
+ {
+ MoveWindow(hWnd, rWnd.left, rWnd.top, rWnd.right - rWnd.left, rWnd.bottom - rWnd.top, TRUE);
+ }
+
+ const ext::string& getMirandaPath()
+ {
+ static ext::string strMirandaPath;
+
+ if (strMirandaPath.empty())
+ {
+ mu_text szPath[MAX_PATH] = { 0 };
+
+ mu::utils::pathToAbsolute(muT("x"), szPath);
+ strMirandaPath = extractPath(szPath);
+ }
+
+ return strMirandaPath;
+ }
+
+ const ext::string& getProfilePath()
+ {
+ static ext::string strProfilePath;
+
+ if (strProfilePath.empty())
+ {
+ mu_text szPath[MAX_PATH] = { 0 };
+
+ mu::db::getProfilePath(MAX_PATH, szPath);
+ strProfilePath = szPath;
+
+ if (strProfilePath.empty() || strProfilePath[strProfilePath.length() - 1] != muC('\\'))
+ {
+ strProfilePath += muT("\\");
+ }
+ }
+
+ return strProfilePath;
+ }
+
+ const ext::string& getProfileName()
+ {
+ static ext::string strProfileName;
+
+ if (strProfileName.empty())
+ {
+ mu_text szName[MAX_PATH] = { 0 };
+
+ mu::db::getProfileName(MAX_PATH, szName);
+ strProfileName = szName;
+
+ ext::string::size_type posDot = strProfileName.rfind(muC('.'));
+
+ if (posDot != ext::string::npos && posDot != 0)
+ {
+ strProfileName.erase(posDot);
+ }
+ }
+
+ return strProfileName;
+ }
+};
+
+/*
+ * OS
+ */
+
+OS::OS()
+ : m_bIsXPPlus(false),
+ m_ImageListColor(ILC_COLORDDB) // MEMO: maybe change this to ILC_COLOR{8,16,24}
+{
+ m_SmIcon.cx = 16; // GetSystemMetrics(SM_CXSMICON);
+ m_SmIcon.cy = 16; // GetSystemMetrics(SM_CYSMICON);
+
+ OSVERSIONINFO osvi = { 0 };
+
+ osvi.dwOSVersionInfoSize = sizeof(osvi);
+
+ if (GetVersionEx(&osvi))
+ {
+ m_bIsXPPlus = ((osvi.dwMajorVersion == 5 && osvi.dwMinorVersion >= 1) || osvi.dwMajorVersion >= 6);
+
+ if (m_bIsXPPlus)
+ {
+ m_ImageListColor = ILC_COLOR32;
+ }
+ }
+}
+
+OS OS::m_Data;
+
+/*
+ * Locale
+ */
+
+void Locale::init()
+{
+ setlocale(LC_ALL, muA(""));
+ // setlocale(LC_ALL, muA("French_France"));
+ // setlocale(LC_ALL, muA("English_USA"));
+ // setlocale(LC_ALL, muA("Russian_Russia"));
+
+ m_Data.m_ThousandSep = utils::fromA(localeconv()->thousands_sep).c_str()[0];
+ m_Data.m_DecimalPoint = utils::fromA(localeconv()->decimal_point).c_str()[0];
+ m_Data.m_Grouping = localeconv()->grouping;
+}
+
+Locale Locale::m_Data;
+
+/*
+ * RTFFilter
+ */
+
+RTFFilter::RTFFilter()
+ : m_hRTFConv(NULL)
+{
+}
+
+void RTFFilter::init()
+{
+ if (!(m_Data.m_hRTFConv = LoadLibrary(muT("rtfconv.dll"))))
+ {
+ if (!(m_Data.m_hRTFConv = LoadLibrary(muT("plugins\\rtfconv.dll"))))
+ {
+ return;
+ }
+ }
+
+ if (!(m_Data.m_RTFConvString = reinterpret_cast<RTFCONVSTRING>(GetProcAddress(m_Data.m_hRTFConv, muA("RtfconvString")))))
+ {
+ FreeLibrary(m_Data.m_hRTFConv);
+
+ m_Data.m_hRTFConv = NULL;
+ }
+
+ InitializeCriticalSection(&m_Data.m_RTFConvCS);
+}
+
+void RTFFilter::uninit()
+{
+ if (m_Data.m_hRTFConv)
+ {
+ DeleteCriticalSection(&m_Data.m_RTFConvCS);
+ FreeLibrary(m_Data.m_hRTFConv);
+
+ m_Data.m_hRTFConv = NULL;
+ m_Data.m_RTFConvString = NULL;
+ }
+}
+
+ext::t::string RTFFilter::filter(const ext::t::string& str)
+{
+ // protect, because library is not thread-safe
+ EnterCriticalSection(&m_Data.m_RTFConvCS);
+
+#if defined(MU_WIDE)
+ const ext::a::string strA = utils::toA(str);
+#else // MU_WIDE
+ const ext::a::string& strA = str;
+#endif // MU_WIDE
+
+ intptr_t len = m_Data.m_RTFConvString(
+ strA.c_str(),
+ NULL,
+ 0,
+ MU_DO_BOTH(GetACP(), CP_UNICODE),
+ CONVMODE_USE_SYSTEM_TABLE | MU_DO_BOTH(0, CONVMODE_NO_OUTPUT_BOM),
+ 0);
+
+ if (len == -1)
+ {
+ // someting went wrong, maybe it's not a real RTF string
+ LeaveCriticalSection(&m_Data.m_RTFConvCS);
+
+ return str;
+ }
+
+ mu_text* out_buf = new mu_text[len / sizeof(mu_text)];
+
+ intptr_t res = m_Data.m_RTFConvString(
+ strA.c_str(),
+ out_buf,
+ 0,
+ MU_DO_BOTH(GetACP(), CP_UNICODE),
+ CONVMODE_USE_SYSTEM_TABLE | MU_DO_BOTH(0, CONVMODE_NO_OUTPUT_BOM),
+ len);
+
+ if (res == -1)
+ {
+ // someting went wrong, maybe it's not a real RTF string
+ delete[] out_buf;
+
+ LeaveCriticalSection(&m_Data.m_RTFConvCS);
+
+ return str;
+ }
+
+ ext::t::string out_str(out_buf, res / sizeof(mu_text) - 1);
+ delete[] out_buf;
+
+ LeaveCriticalSection(&m_Data.m_RTFConvCS);
+
+ return out_str;
+}
+
+RTFFilter RTFFilter::m_Data;
diff --git a/plugins/HistoryStats/src/utils.h b/plugins/HistoryStats/src/utils.h
new file mode 100644
index 0000000000..aa2e5a57a0
--- /dev/null
+++ b/plugins/HistoryStats/src/utils.h
@@ -0,0 +1,179 @@
+#if !defined(HISTORYSTATS_GUARD_UTILS_H)
+#define HISTORYSTATS_GUARD_UTILS_H
+
+#include "_globals.h"
+#include "_consts.h"
+
+namespace utils
+{
+ // time formatting
+ ext::string timestampToString(DWORD value, const mu_text* format);
+ ext::string tmStructToString(const tm& value, const mu_text* format);
+ inline ext::string timestampToDateTime(DWORD value) { return timestampToString(value, muT("%c")); }
+ inline ext::string timestampToDate(DWORD value) { return timestampToString(value, muT("%x")); }
+ inline ext::string timestampToTime(DWORD value) { return timestampToString(value, muT("%X")); }
+ ext::string durationToString(DWORD value);
+ DWORD parseDate(const ext::string& date);
+ inline ext::string formatDate(DWORD dwTimestamp) { return timestampToString(dwTimestamp, muT("%Y-%m-%d")); }
+
+ // number formatting
+ ext::string intToString(int value);
+ ext::string intToPadded(int value, int len);
+ ext::string intToGrouped(int value);
+ ext::string floatToString(double value, int precision);
+ ext::string floatToGrouped(double value, int precision);
+ inline ext::string ratioToString(int numerator, int denominator, int precision) { return floatToString((denominator != 0) ? (1.0 * numerator / denominator) : 0.0, precision); }
+ inline ext::string ratioToGrouped(int numerator, int denominator, int precision) { return floatToGrouped((denominator != 0) ? (1.0 * numerator / denominator) : 0.0, precision); }
+ ext::string ratioToPercent(int numerator, int denominator);
+
+ // text conversion
+ void replaceAllInPlace(ext::string& text, const mu_text* find, const mu_text* replace);
+ void htmlEscapeInPlace(ext::string& text);
+ inline ext::string replaceAll(ext::string text, const mu_text* find, const mu_text* replace) { replaceAllInPlace(text, find, replace); return text; }
+ inline ext::string htmlEscape(ext::string text) { htmlEscapeInPlace(text); return text; }
+ const mu_text* stripPrefix(const mu_text* szPrefix, const mu_text* szText);
+ ext::string replaceVariables(const ext::string& strFormat, DWORD timeValue, const mu_text* szNick = muT(""));
+
+ // case conversion
+ ext::string toLowerCase(const ext::string& text);
+ ext::string toUpperCase(const ext::string& text);
+
+ // time conversion
+ inline DWORD toLocalTime(DWORD gmtTimestamp) { return mu::db_time::timestampToLocal(gmtTimestamp); }
+
+ // version conversion
+ DWORD dottedToVersion(ext::string version);
+ ext::string versionToDotted(DWORD version);
+
+ // character conversion (wide, ansi, utf8)
+ ext::a::string convertWToA(const mu_wide* str, size_t len);
+ ext::w::string convertAToW(const mu_ansi* str, size_t len);
+ ext::a::string convertTToUTF8(const mu_text* str, size_t str_len);
+ ext::string convertUTF8ToT(const mu_ansi* str, size_t str_len);
+ size_t rawUTF8Encode(const mu_wide* pIn, size_t lenIn, mu_ansi* pOut);
+ size_t getUTF8Len(const mu_ansi* str);
+
+ // character conversion (convenience functions)
+ inline ext::a::string toA(const mu_text* str) { return MU_DO_BOTH(str, convertWToA(str, ext::strfunc::len(str))); }
+ inline ext::w::string toW(const mu_text* str) { return MU_DO_BOTH(convertAToW(str, ext::strfunc::len(str)), str); }
+ inline ext::string fromA(const mu_ansi* str) { return MU_DO_BOTH(str, convertAToW(str, ext::a::strfunc::len(str))); }
+ inline ext::string fromW(const mu_wide* str) { return MU_DO_BOTH(convertWToA(str, ext::w::strfunc::len(str)), str); }
+ inline ext::a::string toA(const ext::string& str) { return MU_DO_BOTH(str, convertWToA(str.c_str(), str.length())); }
+ inline ext::w::string toW(const ext::string& str) { return MU_DO_BOTH(convertAToW(str.c_str(), str.length()), str); }
+ inline ext::string fromA(const ext::a::string& str) { return MU_DO_BOTH(str, convertAToW(str.c_str(), str.length())); }
+ inline ext::string fromW(const ext::w::string& str) { return MU_DO_BOTH(convertWToA(str.c_str(), str.length()), str); }
+ inline ext::a::string toUTF8(const mu_text* str) { return convertTToUTF8(str, ext::strfunc::len(str)); }
+ inline ext::string fromUTF8(const mu_ansi* str) { return convertUTF8ToT(str, ext::a::strfunc::len(str)); }
+ inline ext::a::string toUTF8(const ext::string& str) { return convertTToUTF8(str.c_str(), str.length()); }
+ inline ext::string fromUTF8(const ext::a::string& str) { return convertUTF8ToT(str.c_str(), str.length()); }
+
+ // file management
+ bool fileExists(const ext::string& fileName);
+ bool pathExists(const ext::string& path);
+ bool isRelative(const ext::string& fileName);
+ bool isValidFilePart(ext::string filePart);
+ bool isValidFile(const ext::string& fileName);
+ ext::string extractPath(const ext::string& fileName);
+ ext::string extractFile(const ext::string& fileName);
+ bool createPath(const ext::string& path);
+
+ // color management
+ ext::string colorToHTML(COLORREF crColor);
+ void generateGradient(COLORREF fromColor, COLORREF toColor, COLORREF colorTab[256]);
+
+ // drawing helpers
+ inline POINT point(int x, int y) { POINT p = { x, y }; return p; }
+ inline RECT rect(int left, int top, int right, int bottom) { RECT r = { left, top, right, bottom }; return r; }
+
+ // misc functionality
+ void ensureRange(int& value, int min, int max, int fallback);
+ void ensureRange(unsigned int& value, unsigned int min, unsigned int max, unsigned int fallback);
+ ext::string getGUID();
+
+ // window positioning
+ void centerDialog(HWND hDlg, HWND hParent = NULL);
+ RECT getWindowRect(HWND hParent, HWND hWnd);
+ inline RECT getWindowRect(HWND hParent, int nID) { return getWindowRect(hParent, GetDlgItem(hParent, nID)); }
+ void moveWindow(HWND hWnd, const RECT& rWnd);
+ inline void moveWindow(HWND hParent, int nID, const RECT& rWnd) { moveWindow(GetDlgItem(hParent, nID), rWnd); }
+
+ // cached access to miranda properties
+ const ext::string& getMirandaPath();
+ const ext::string& getProfilePath();
+ const ext::string& getProfileName();
+}
+
+class OS
+ : private pattern::NotCopyable<OS>
+{
+private:
+ bool m_bIsXPPlus;
+ UINT m_ImageListColor;
+ SIZE m_SmIcon;
+
+private:
+ explicit OS();
+
+private:
+ static OS m_Data;
+
+public:
+ static bool isXPPlus() { return m_Data.m_bIsXPPlus; }
+ static UINT imageListColor() { return m_Data.m_ImageListColor; }
+ static int smIconCX() { return m_Data.m_SmIcon.cx; }
+ static int smIconCY() { return m_Data.m_SmIcon.cy; }
+};
+
+class Locale
+ : private pattern::NotCopyable<Locale>
+{
+private:
+ mu_text m_DecimalPoint;
+ mu_text m_ThousandSep;
+ ext::a::string m_Grouping;
+
+private:
+ explicit Locale() { }
+
+private:
+ static Locale m_Data;
+
+public:
+ static void init();
+ static mu_text decimalPoint() { return m_Data.m_DecimalPoint; }
+ static mu_text thousandSep() { return m_Data.m_ThousandSep; }
+ static const char* grouping() { return m_Data.m_Grouping.c_str(); }
+};
+
+class RTFFilter
+ : private pattern::NotCopyable<RTFFilter>
+{
+private:
+ typedef intptr_t (WINAPI *RTFCONVSTRING)(const void *pSrcBuffer, void *pDstBuffer, int nSrcCodePage, int nDstCodePage, unsigned long dwFlags, size_t nMaxLen);
+
+private:
+ enum {
+ CP_UNICODE = 1200,
+ CONVMODE_NO_OUTPUT_BOM = 0x20000u,
+ CONVMODE_USE_SYSTEM_TABLE = 0x800000u,
+ };
+
+private:
+ HMODULE m_hRTFConv;
+ RTFCONVSTRING m_RTFConvString;
+ CRITICAL_SECTION m_RTFConvCS;
+
+private:
+ explicit RTFFilter();
+
+private:
+ static RTFFilter m_Data;
+
+public:
+ static void init();
+ static void uninit();
+ static bool available() { return !!m_Data.m_hRTFConv; }
+ static ext::t::string filter(const ext::t::string& str);
+};
+
+#endif // HISTORYSTATS_GUARD_UTILS_H \ No newline at end of file
diff --git a/plugins/HistoryStats/src/utils/pattern.h b/plugins/HistoryStats/src/utils/pattern.h
new file mode 100644
index 0000000000..f8ce4996a7
--- /dev/null
+++ b/plugins/HistoryStats/src/utils/pattern.h
@@ -0,0 +1,39 @@
+#if !defined(HISTORYSTATS_GUARD_UTILS_PATTERN_H)
+#define HISTORYSTATS_GUARD_UTILS_PATTERN_H
+
+namespace pattern
+{
+ template<typename T_>
+ class NotCopyable
+ {
+ private:
+ NotCopyable(const NotCopyable&);
+ const NotCopyable& operator =(const NotCopyable&);
+
+ protected:
+ NotCopyable()
+ {
+ }
+
+ ~NotCopyable()
+ {
+ }
+ };
+
+ template<typename T_>
+ class NotInstantiable
+ {
+ private:
+ NotInstantiable(const NotInstantiable&);
+ const NotInstantiable& operator =(const NotInstantiable&);
+
+ protected:
+ NotInstantiable();
+
+ ~NotInstantiable()
+ {
+ }
+ };
+}
+
+#endif // HISTORYSTATS_GUARD_UTILS_PATTERN_H \ No newline at end of file