From 238356406f86c7d0f7153b69c9918e536cb88ce9 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 31 Oct 2012 19:49:29 -0700 Subject: [PATCH] Extract calltip logic from the edit ctrl to libaegisub --- aegisub/build/libaegisub/libaegisub.vcxproj | 2 + .../libaegisub/libaegisub.vcxproj.filters | 6 + aegisub/libaegisub/Makefile | 1 + aegisub/libaegisub/ass/dialogue_parser.cpp | 5 +- .../libaegisub/common/calltip_provider.cpp | 186 ++++++++++++ .../include/libaegisub/calltip_provider.h | 46 +++ aegisub/src/subs_edit_ctrl.cpp | 272 +----------------- aegisub/src/subs_edit_ctrl.h | 12 +- 8 files changed, 266 insertions(+), 264 deletions(-) create mode 100644 aegisub/libaegisub/common/calltip_provider.cpp create mode 100644 aegisub/libaegisub/include/libaegisub/calltip_provider.h diff --git a/aegisub/build/libaegisub/libaegisub.vcxproj b/aegisub/build/libaegisub/libaegisub.vcxproj index 870e9ffdf..5600a1995 100644 --- a/aegisub/build/libaegisub/libaegisub.vcxproj +++ b/aegisub/build/libaegisub/libaegisub.vcxproj @@ -92,6 +92,7 @@ + @@ -123,6 +124,7 @@ + diff --git a/aegisub/build/libaegisub/libaegisub.vcxproj.filters b/aegisub/build/libaegisub/libaegisub.vcxproj.filters index 04d67508d..4539c7db6 100644 --- a/aegisub/build/libaegisub/libaegisub.vcxproj.filters +++ b/aegisub/build/libaegisub/libaegisub.vcxproj.filters @@ -137,6 +137,9 @@ Header Files + + Header Files + @@ -220,6 +223,9 @@ Source Files\Common + + Source Files\Common + diff --git a/aegisub/libaegisub/Makefile b/aegisub/libaegisub/Makefile index 49e9c9191..9e46d425b 100644 --- a/aegisub/libaegisub/Makefile +++ b/aegisub/libaegisub/Makefile @@ -20,6 +20,7 @@ SRC += \ common/cajun/elements.cpp \ common/cajun/reader.cpp \ common/cajun/writer.cpp \ + common/calltip_provider.cpp \ common/charset.cpp \ common/charset_6937.cpp \ common/charset_conv.cpp \ diff --git a/aegisub/libaegisub/ass/dialogue_parser.cpp b/aegisub/libaegisub/ass/dialogue_parser.cpp index b0010e3c2..14ef7e863 100644 --- a/aegisub/libaegisub/ass/dialogue_parser.cpp +++ b/aegisub/libaegisub/ass/dialogue_parser.cpp @@ -59,7 +59,8 @@ class SyntaxHighlighter { char *outptr = (char *)&chr; size_t outlen = sizeof chr; - if (iconv(utf8_to_utf32, &inptr, &inlen, &outptr, &outlen) != 1) + iconv(utf8_to_utf32, &inptr, &inlen, &outptr, &outlen); + if (outlen != 0) return 0; char_len = len - inlen; @@ -105,7 +106,7 @@ public: SyntaxHighlighter(std::string const& text, agi::SpellChecker *spellchecker) : text(text) , spellchecker(spellchecker) - , utf8_to_utf32(iconv_open("utf-32", "utf-8"), iconv_close) + , utf8_to_utf32(iconv_open("utf-32le", "utf-8"), iconv_close) { } TokenVec Highlight(TokenVec const& tokens, bool template_line) { diff --git a/aegisub/libaegisub/common/calltip_provider.cpp b/aegisub/libaegisub/common/calltip_provider.cpp new file mode 100644 index 000000000..b3272e688 --- /dev/null +++ b/aegisub/libaegisub/common/calltip_provider.cpp @@ -0,0 +1,186 @@ +// Copyright (c) 2012, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#include "../config.h" + +#include "libaegisub/calltip_provider.h" + +#include "libaegisub/ass/dialogue_parser.h" + +#include + +namespace { + struct proto_lit { + const char *name; + bool has_parens; + const char *args; + }; + + proto_lit calltip_protos[] = { + { "move", true, "X1\0Y1\0X2\0Y2\0" }, + { "move", true, "X1\0Y1\0X2\0Y2\0Start Time\0End Time\0" }, + { "fn", false, "Font Name\0" }, + { "bord", false, "Width\0" }, + { "xbord", false, "Width\0" }, + { "ybord", false, "Width\0" }, + { "shad", false, "Depth\0" }, + { "xshad", false, "Depth\0" }, + { "yshad", false, "Depth\0" }, + { "be", false, "Strength\0" }, + { "blur", false, "Strength\0" }, + { "fscx", false, "Scale\0" }, + { "fscy", false, "Scale\0" }, + { "fsp", false, "Spacing\0" }, + { "fs", false, "Font Size\0" }, + { "fe", false, "Encoding\0" }, + { "frx", false, "Angle\0" }, + { "fry", false, "Angle\0" }, + { "frz", false, "Angle\0" }, + { "fr", false, "Angle\0" }, + { "pbo", false, "Offset\0" }, + { "clip", true, "Command\0" }, + { "clip", true, "Scale\0Command\0" }, + { "clip", true, "X1\0Y1\0X2\0Y2\0" }, + { "iclip", true, "Command\0" }, + { "iclip", true, "Scale\0Command\0" }, + { "iclip", true, "X1\0Y1\0X2\0Y2\0" }, + { "t", true, "Acceleration\0Tags\0" }, + { "t", true, "Start Time\0End Time\0Tags\0" }, + { "t", true, "Start Time\0End Time\0Acceleration\0Tags\0" }, + { "pos", true, "X\0Y\0" }, + { "p", false, "Exponent\0" }, + { "org", true, "X\0Y\0" }, + { "fade", true, "Start Alpha\0Middle Alpha\0End Alpha\0Start In\0End In\0Start Out\0End Out\0" }, + { "fad", true, "Start Time\0End Time\0" }, + { "c", false, "Colour\0" }, + { "1c", false, "Colour\0" }, + { "2c", false, "Colour\0" }, + { "3c", false, "Colour\0" }, + { "4c", false, "Colour\0" }, + { "alpha", false, "Alpha\0" }, + { "1a", false, "Alpha\0" }, + { "2a", false, "Alpha\0" }, + { "3a", false, "Alpha\0" }, + { "4a", false, "Alpha\0" }, + { "an", false, "Alignment\0" }, + { "a", false, "Alignment\0" }, + { "b", false, "Weight\0" }, + { "i", false, "1/0\0" }, + { "u", false, "1/0\0" }, + { "s", false, "1/0\0" }, + { "kf", false, "Duration\0" }, + { "ko", false, "Duration\0" }, + { "k", false, "Duration\0" }, + { "K", false, "Duration\0" }, + { "q", false, "Wrap Style\0" }, + { "r", false, "Style\0" }, + { "fax", false, "Factor\0" }, + { "fay", false, "Factor\0" } + }; +} + +namespace agi { +CalltipProvider::CalltipProvider() { + for (auto proto : calltip_protos) { + CalltipProto p; + std::string tag_name = proto.name; + p.text = '\\' + tag_name; + if (proto.has_parens) + p.text += '('; + + for (const char *arg = proto.args; *arg; ) { + size_t start = p.text.size(); + p.text += arg; + size_t end = p.text.size(); + if (proto.has_parens) + p.text += ','; + + arg += end - start + 1; + p.args.emplace_back(start, end); + } + + // replace trailing comma + if (proto.has_parens) + p.text.back() = ')'; + + protos.emplace(std::move(tag_name), std::move(p)); + } +} + +Calltip CalltipProvider::GetCalltip(std::vector const& tokens, std::string const& text, size_t pos) { + namespace dt = ass::DialogueTokenType; + + Calltip ret = { "", 0, 0, 0 }; + + size_t idx = 0; + size_t tag_name_idx = 0; + size_t commas = 0; + for (; idx < tokens.size() && pos >= tokens[idx].length; ++idx) { + switch (tokens[idx].type) { + case dt::COMMENT: + case dt::OVR_END: + tag_name_idx = 0; + break; + case dt::TAG_NAME: + tag_name_idx = idx; + commas = 0; + break; + case dt::ARG_SEP: + ++commas; + break; + default: break; + } + pos -= tokens[idx].length; + } + + // Either didn't hit a tag or the override block ended before reaching the + // current position + if (tag_name_idx == 0) + return ret; + + // Find the prototype for this tag + size_t tag_name_start = 0; + for (size_t i = 0; i < tag_name_idx; ++i) + tag_name_start += tokens[i].length; + auto it = protos.equal_range(text.substr(tag_name_start, tokens[tag_name_idx].length)); + + // If there's multiple overloads, check how many total arguments we have + // and pick the one with the least args >= current arg count + if (distance(it.first, it.second) > 1) { + size_t args = commas + 1; + for (size_t i = idx; i < tokens.size(); ++i) { + int type = tokens[i].type; + if (type == dt::ARG_SEP) + ++args; + else if (type != dt::ARG && type != dt::WHITESPACE) + break; + } + + while (it.first != it.second && args > it.first->second.args.size()) + ++it.first; + } + + // Unknown tag or too many arguments + if (it.first == it.second || it.first->second.args.size() <= commas) + return ret; + + ret.text = it.first->second.text; + ret.highlight_start = it.first->second.args[commas].first; + ret.highlight_end = it.first->second.args[commas].second; + ret.tag_position = tag_name_start; + return ret; +} +} diff --git a/aegisub/libaegisub/include/libaegisub/calltip_provider.h b/aegisub/libaegisub/include/libaegisub/calltip_provider.h new file mode 100644 index 000000000..af7ab734e --- /dev/null +++ b/aegisub/libaegisub/include/libaegisub/calltip_provider.h @@ -0,0 +1,46 @@ +// Copyright (c) 2012, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#ifndef LAGI_PRE +#include +#include +#include +#endif + +namespace agi { + namespace ass { struct DialogueToken; } + + struct Calltip { + std::string text; ///< Text of the calltip + size_t highlight_start; ///< Start index of the current parameter in text + size_t highlight_end; ///< End index of the current parameter in text + size_t tag_position; ///< Start index of the tag in the input line + }; + + class CalltipProvider { + struct CalltipProto { + std::string text; + std::vector> args; + }; + std::multimap protos; + + public: + CalltipProvider(); + + /// Get the calltip to show for the given cursor position in the text + Calltip GetCalltip(std::vector const& tokens, std::string const& text, size_t pos); + }; +} diff --git a/aegisub/src/subs_edit_ctrl.cpp b/aegisub/src/subs_edit_ctrl.cpp index 803fc997d..5058a4b78 100644 --- a/aegisub/src/subs_edit_ctrl.cpp +++ b/aegisub/src/subs_edit_ctrl.cpp @@ -57,6 +57,7 @@ #include "utils.h" #include +#include #include /// Event ids @@ -82,6 +83,7 @@ SubsTextEditCtrl::SubsTextEditCtrl(wxWindow* parent, wxSize wsize, long style, a : ScintillaTextCtrl(parent, -1, "", wxDefaultPosition, wsize, style) , spellchecker(SpellCheckerFactory::GetSpellChecker()) , context(context) +, calltip_position(0) { // Set properties SetWrapMode(wxSTC_WRAP_WORD); @@ -101,68 +103,6 @@ SubsTextEditCtrl::SubsTextEditCtrl(wxWindow* parent, wxSize wsize, long style, a CmdKeyClear('T',wxSTC_SCMOD_CTRL | wxSTC_SCMOD_SHIFT); CmdKeyClear('U',wxSTC_SCMOD_CTRL); - // Prototypes for call tips - tipProtoN = -1; - proto.Add("move(x1,y1,x2,y2)"); - proto.Add("move(x1,y1,x2,y2,startTime,endTime)"); - proto.Add("fn;FontName"); - proto.Add("bord;Width"); - proto.Add("xbord;Width"); - proto.Add("ybord;Width"); - proto.Add("shad;Depth"); - proto.Add("xshad;Depth"); - proto.Add("yshad;Depth"); - proto.Add("be;Strength"); - proto.Add("blur;Strength"); - proto.Add("fscx;Scale"); - proto.Add("fscy;Scale"); - proto.Add("fsp;Spacing"); - proto.Add("fs;FontSize"); - proto.Add("fe;Encoding"); - proto.Add("frx;Angle"); - proto.Add("fry;Angle"); - proto.Add("frz;Angle"); - proto.Add("fr;Angle"); - proto.Add("pbo;Offset"); - proto.Add("clip(command)"); - proto.Add("clip(scale,command)"); - proto.Add("clip(x1,y1,x2,y2)"); - proto.Add("iclip(command)"); - proto.Add("iclip(scale,command)"); - proto.Add("iclip(x1,y1,x2,y2)"); - proto.Add("t(acceleration,tags)"); - proto.Add("t(startTime,endTime,tags)"); - proto.Add("t(startTime,endTime,acceleration,tags)"); - proto.Add("pos(x,y)"); - proto.Add("p;Exponent"); - proto.Add("org(x,y)"); - proto.Add("fade(startAlpha,middleAlpha,endAlpha,startIn,endIn,startOut,endOut)"); - proto.Add("fad(startTime,endTime)"); - proto.Add("c;Colour"); - proto.Add("1c;Colour"); - proto.Add("2c;Colour"); - proto.Add("3c;Colour"); - proto.Add("4c;Colour"); - proto.Add("alpha;Alpha"); - proto.Add("1a;Alpha"); - proto.Add("2a;Alpha"); - proto.Add("3a;Alpha"); - proto.Add("4a;Alpha"); - proto.Add("an;Alignment"); - proto.Add("a;Alignment"); - proto.Add("b;Weight"); - proto.Add("i;1/0"); - proto.Add("u;1/0"); - proto.Add("s;1/0"); - proto.Add("kf;Duration"); - proto.Add("ko;Duration"); - proto.Add("k;Duration"); - proto.Add("K;Duration"); - proto.Add("q;WrapStyle"); - proto.Add("r;Style"); - proto.Add("fax;Factor"); - proto.Add("fay;Factor"); - using std::bind; Bind(wxEVT_CHAR_HOOK, &SubsTextEditCtrl::OnKeyDown, this); @@ -292,211 +232,27 @@ void SubsTextEditCtrl::UpdateCallTip(wxStyledTextEvent &) { if (!OPT_GET("App/Call Tips")->GetBool()) return; - // Get position and text - const unsigned int pos = GetReverseUnicodePosition(GetCurrentPos()); - wxString text = GetText(); + if (!calltip_provider) + calltip_provider.reset(new agi::CalltipProvider); - // Find the start and end of current tag - wxChar prevChar = 0; - int depth = 0; - int inDepth = 0; - int tagStart = -1; - int tagEnd = -1; - for (unsigned int i=0;i= pos && depth == 0) { - tagEnd = i-1; - break; - } - continue; - } + agi::Calltip new_calltip = calltip_provider->GetCalltip(tokens, text, GetCurrentPos()); - // Outside - if (depth == 0) { - tagStart = -1; - if (i == pos) break; - continue; - } - - // Inside overrides - if (depth == 1) { - // Inner depth - if (tagStart != -1) { - if (curChar == '(') inDepth++; - else if (curChar == ')') inDepth--; - } - - // Not inside parenthesis - if (inDepth == 0) { - if (prevChar == '\\') { - // Found start - if (i <= pos) tagStart = i; - - // Found end - else { - tagEnd = i-2; - break; - } - } - } - } - - // Previous character - prevChar = curChar; - } - - // Calculate length - int len; - if (tagEnd != -1) len = tagEnd - tagStart + 1; - else len = text.Length() - tagStart; - - // No tag available - int textLen = text.Length(); - unsigned int posInTag = pos - tagStart; - if (tagStart+len > textLen || len <= 0 || tagStart < 0) { + if (new_calltip.text.empty()) { CallTipCancel(); return; } - // Current tag - wxString tag = text.Mid(tagStart,len); + if (!CallTipActive() || calltip_position != new_calltip.tag_position || calltip_text != new_calltip.text) + CallTipShow(new_calltip.tag_position, to_wx(new_calltip.text)); - // Metrics in tag - int tagCommas = 0; - int tagParenthesis = 0; - int parN = 0; - int parPos = -1; - bool gotName = false; - wxString tagName = tag; - for (unsigned int i=0;i 0) { - cleanProto.Replace(";",""); - semiProto = true; - } - - // Prototype match - wxString temp; - if (semiProto) temp = tagName.Left(protoName.Length()); - else temp = tagName; - if (protoName == temp) { - // Parameter count match - if (proto[i].Freq(',') >= tagCommas) { - // Found - useProto = proto[i]; - protoN = i; - break; - } - } - } - - // No matching prototype - if (useProto.IsEmpty()) { - CallTipCancel(); - return; - } - - // Parameter number for tags without "()," - if (semiProto && parPos == 0 && posInTag >= protoName.Length()) parPos = 1; - - // Highlight start/end - int highStart = useProto.Length(); - int highEnd = -1; - parN = 0; - int delta = 0; - for (unsigned int i=0;i thesaurus; + agi::scoped_ptr calltip_provider; + /// Project context, for splitting lines agi::Context *context; @@ -72,11 +75,12 @@ class SubsTextEditCtrl : public ScintillaTextCtrl { /// Thesaurus suggestions for the last right-clicked word std::vector thesSugs; - /// Calltip prototypes - wxArrayString proto; + /// Text of the currently shown calltip, to avoid flickering from + /// pointlessly reshowing the current tip + std::string calltip_text; - /// Last show calltip, to avoid flickering the currently active one - int tipProtoN; + /// Position of the currently show calltip + size_t calltip_position; void OnContextMenu(wxContextMenuEvent &); void OnAddToDictionary(wxCommandEvent &event);