@@ -0,0 +1,119 @@ | |||
#!/usr/bin/make -f | |||
# Makefile for juce_gui_extra # | |||
# --------------------------- # | |||
# Created by falkTX | |||
# | |||
CWD=../.. | |||
MODULENAME=juce_gui_extra | |||
include ../Makefile.mk | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
BUILD_CXX_FLAGS += $(JUCE_GUI_EXTRA_FLAGS) -I.. | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
ifeq ($(MACOS),true) | |||
OBJS = $(OBJDIR)/$(MODULENAME).mm.o | |||
OBJS_posix32 = $(OBJDIR)/$(MODULENAME).mm.posix32.o | |||
OBJS_posix64 = $(OBJDIR)/$(MODULENAME).mm.posix64.o | |||
else | |||
OBJS = $(OBJDIR)/$(MODULENAME).cpp.o | |||
OBJS_posix32 = $(OBJDIR)/$(MODULENAME).cpp.posix32.o | |||
OBJS_posix64 = $(OBJDIR)/$(MODULENAME).cpp.posix64.o | |||
endif | |||
OBJS_win32 = $(OBJDIR)/$(MODULENAME).cpp.win32.o | |||
OBJS_win64 = $(OBJDIR)/$(MODULENAME).cpp.win64.o | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
all: $(MODULEDIR)/$(MODULENAME).a | |||
posix32: $(MODULEDIR)/$(MODULENAME).posix32.a | |||
posix64: $(MODULEDIR)/$(MODULENAME).posix64.a | |||
win32: $(MODULEDIR)/$(MODULENAME).win32.a | |||
win64: $(MODULEDIR)/$(MODULENAME).win64.a | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
clean: | |||
rm -f $(OBJDIR)/*.o $(MODULEDIR)/$(MODULENAME)*.a | |||
debug: | |||
$(MAKE) DEBUG=true | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
$(MODULEDIR)/$(MODULENAME).a: $(OBJS) | |||
-@mkdir -p $(MODULEDIR) | |||
@echo "Creating $(MODULENAME).a" | |||
@rm -f $@ | |||
@$(AR) crs $@ $^ | |||
$(MODULEDIR)/$(MODULENAME).posix32.a: $(OBJS_posix32) | |||
-@mkdir -p $(MODULEDIR) | |||
@echo "Creating $(MODULENAME).posix32.a" | |||
@rm -f $@ | |||
@$(AR) crs $@ $^ | |||
$(MODULEDIR)/$(MODULENAME).posix64.a: $(OBJS_posix64) | |||
-@mkdir -p $(MODULEDIR) | |||
@echo "Creating $(MODULENAME).posix64.a" | |||
@rm -f $@ | |||
@$(AR) crs $@ $^ | |||
$(MODULEDIR)/$(MODULENAME).win32.a: $(OBJS_win32) | |||
-@mkdir -p $(MODULEDIR) | |||
@echo "Creating $(MODULENAME).win32.a" | |||
@rm -f $@ | |||
@$(AR) crs $@ $^ | |||
$(MODULEDIR)/$(MODULENAME).win64.a: $(OBJS_win64) | |||
-@mkdir -p $(MODULEDIR) | |||
@echo "Creating $(MODULENAME).win64.a" | |||
@rm -f $@ | |||
@$(AR) crs $@ $^ | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
$(OBJDIR)/$(MODULENAME).cpp.o: $(MODULENAME).cpp | |||
-@mkdir -p $(OBJDIR) | |||
@echo "Compiling $<" | |||
@$(CXX) $< $(BUILD_CXX_FLAGS) -c -o $@ | |||
$(OBJDIR)/$(MODULENAME).cpp.%32.o: $(MODULENAME).cpp | |||
-@mkdir -p $(OBJDIR) | |||
@echo "Compiling $< (32bit)" | |||
@$(CXX) $< $(BUILD_CXX_FLAGS) $(32BIT_FLAGS) -c -o $@ | |||
$(OBJDIR)/$(MODULENAME).cpp.%64.o: $(MODULENAME).cpp | |||
-@mkdir -p $(OBJDIR) | |||
@echo "Compiling $< (64bit)" | |||
@$(CXX) $< $(BUILD_CXX_FLAGS) $(64BIT_FLAGS) -c -o $@ | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
$(OBJDIR)/$(MODULENAME).mm.o: $(MODULENAME).cpp | |||
-@mkdir -p $(OBJDIR) | |||
@echo "Compiling $<" | |||
@$(CXX) $< $(BUILD_CXX_FLAGS) -ObjC++ -c -o $@ | |||
$(OBJDIR)/$(MODULENAME).mm.%32.o: $(MODULENAME).cpp | |||
-@mkdir -p $(OBJDIR) | |||
@echo "Compiling $< (32bit)" | |||
@$(CXX) $< $(BUILD_CXX_FLAGS) $(32BIT_FLAGS) -ObjC++ -c -o $@ | |||
$(OBJDIR)/$(MODULENAME).mm.%64.o: $(MODULENAME).cpp | |||
-@mkdir -p $(OBJDIR) | |||
@echo "Compiling $< (64bit)" | |||
@$(CXX) $< $(BUILD_CXX_FLAGS) $(64BIT_FLAGS) -ObjC++ -c -o $@ | |||
# ---------------------------------------------------------------------------------------------------------------------------- | |||
-include $(OBJS:%.o=%.d) | |||
-include $(OBJS_posix32:%.o=%.d) | |||
-include $(OBJS_posix64:%.o=%.d) | |||
-include $(OBJS_win32:%.o=%.d) | |||
-include $(OBJS_win64:%.o=%.d) | |||
# ---------------------------------------------------------------------------------------------------------------------------- |
@@ -0,0 +1,75 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
CPlusPlusCodeTokeniser::CPlusPlusCodeTokeniser() {} | |||
CPlusPlusCodeTokeniser::~CPlusPlusCodeTokeniser() {} | |||
int CPlusPlusCodeTokeniser::readNextToken (CodeDocument::Iterator& source) | |||
{ | |||
return CppTokeniserFunctions::readNextToken (source); | |||
} | |||
CodeEditorComponent::ColourScheme CPlusPlusCodeTokeniser::getDefaultColourScheme() | |||
{ | |||
struct Type | |||
{ | |||
const char* name; | |||
uint32 colour; | |||
}; | |||
const Type types[] = | |||
{ | |||
{ "Error", 0xffcc0000 }, | |||
{ "Comment", 0xff00aa00 }, | |||
{ "Keyword", 0xff0000cc }, | |||
{ "Operator", 0xff225500 }, | |||
{ "Identifier", 0xff000000 }, | |||
{ "Integer", 0xff880000 }, | |||
{ "Float", 0xff885500 }, | |||
{ "String", 0xff990099 }, | |||
{ "Bracket", 0xff000055 }, | |||
{ "Punctuation", 0xff004400 }, | |||
{ "Preprocessor Text", 0xff660000 } | |||
}; | |||
CodeEditorComponent::ColourScheme cs; | |||
for (auto& t : types) | |||
cs.set (t.name, Colour (t.colour)); | |||
return cs; | |||
} | |||
bool CPlusPlusCodeTokeniser::isReservedKeyword (const String& token) noexcept | |||
{ | |||
return CppTokeniserFunctions::isReservedKeyword (token.getCharPointer(), token.length()); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,71 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A simple lexical analyser for syntax colouring of C++ code. | |||
@see CodeEditorComponent, CodeDocument | |||
*/ | |||
class JUCE_API CPlusPlusCodeTokeniser : public CodeTokeniser | |||
{ | |||
public: | |||
//============================================================================== | |||
CPlusPlusCodeTokeniser(); | |||
~CPlusPlusCodeTokeniser(); | |||
//============================================================================== | |||
int readNextToken (CodeDocument::Iterator&) override; | |||
CodeEditorComponent::ColourScheme getDefaultColourScheme() override; | |||
/** This is a handy method for checking whether a string is a c++ reserved keyword. */ | |||
static bool isReservedKeyword (const String& token) noexcept; | |||
/** The token values returned by this tokeniser. */ | |||
enum TokenType | |||
{ | |||
tokenType_error = 0, | |||
tokenType_comment, | |||
tokenType_keyword, | |||
tokenType_operator, | |||
tokenType_identifier, | |||
tokenType_integer, | |||
tokenType_float, | |||
tokenType_string, | |||
tokenType_bracket, | |||
tokenType_punctuation, | |||
tokenType_preprocessor | |||
}; | |||
private: | |||
//============================================================================== | |||
JUCE_LEAK_DETECTOR (CPlusPlusCodeTokeniser) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,665 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** Class containing some basic functions for simple tokenising of C++ code. | |||
*/ | |||
struct CppTokeniserFunctions | |||
{ | |||
static bool isIdentifierStart (const juce_wchar c) noexcept | |||
{ | |||
return CharacterFunctions::isLetter (c) | |||
|| c == '_' || c == '@'; | |||
} | |||
static bool isIdentifierBody (const juce_wchar c) noexcept | |||
{ | |||
return CharacterFunctions::isLetterOrDigit (c) | |||
|| c == '_' || c == '@'; | |||
} | |||
static bool isReservedKeyword (String::CharPointerType token, const int tokenLength) noexcept | |||
{ | |||
static const char* const keywords2Char[] = | |||
{ "if", "do", "or", nullptr }; | |||
static const char* const keywords3Char[] = | |||
{ "for", "int", "new", "try", "xor", "and", "asm", "not", nullptr }; | |||
static const char* const keywords4Char[] = | |||
{ "bool", "void", "this", "true", "long", "else", "char", | |||
"enum", "case", "goto", "auto", nullptr }; | |||
static const char* const keywords5Char[] = | |||
{ "float", "const", "while", "break", "false", "catch", "class", "bitor", | |||
"compl", "or_eq", "short", "throw", "union", "using", "final", nullptr }; | |||
static const char* const keywords6Char[] = | |||
{ "return", "and_eq", "bitand", "delete", "double", "export", "extern", | |||
"friend", "inline", "not_eq", "public", "signed", "sizeof", "static", | |||
"struct", "switch", "typeid", "xor_eq", nullptr }; | |||
static const char* const keywords7Char[] = | |||
{ "nullptr", "alignas", "alignof", "default", "mutable", "private", | |||
"typedef", "virtual", "wchar_t", "__cdecl", "_Pragma", "uint8_t", nullptr }; | |||
static const char* const keywordsOther[] = | |||
{ "char16_t", "char32_t", "const_cast", "constexpr", "continue", "decltype", "dynamic_cast", | |||
"explicit", "namespace", "noexcept", "operator", "protected", "register", "reinterpret_cast", | |||
"static_assert", "static_cast", "template", "thread_local", "typename", "unsigned", "volatile", | |||
"@class", "@dynamic", "@end", "@implementation", "@interface", "@public", "@private", | |||
"@protected", "@property", "@synthesize", "__fastcall", "__stdcall", nullptr }; | |||
const char* const* k; | |||
switch (tokenLength) | |||
{ | |||
case 2: k = keywords2Char; break; | |||
case 3: k = keywords3Char; break; | |||
case 4: k = keywords4Char; break; | |||
case 5: k = keywords5Char; break; | |||
case 6: k = keywords6Char; break; | |||
case 7: k = keywords7Char; break; | |||
default: | |||
if (tokenLength < 2 || tokenLength > 16) | |||
return false; | |||
k = keywordsOther; | |||
break; | |||
} | |||
for (int i = 0; k[i] != 0; ++i) | |||
if (token.compare (CharPointer_ASCII (k[i])) == 0) | |||
return true; | |||
return false; | |||
} | |||
template <typename Iterator> | |||
static int parseIdentifier (Iterator& source) noexcept | |||
{ | |||
int tokenLength = 0; | |||
String::CharPointerType::CharType possibleIdentifier [100]; | |||
String::CharPointerType possible (possibleIdentifier); | |||
while (isIdentifierBody (source.peekNextChar())) | |||
{ | |||
const juce_wchar c = source.nextChar(); | |||
if (tokenLength < 20) | |||
possible.write (c); | |||
++tokenLength; | |||
} | |||
if (tokenLength > 1 && tokenLength <= 16) | |||
{ | |||
possible.writeNull(); | |||
if (isReservedKeyword (String::CharPointerType (possibleIdentifier), tokenLength)) | |||
return CPlusPlusCodeTokeniser::tokenType_keyword; | |||
} | |||
return CPlusPlusCodeTokeniser::tokenType_identifier; | |||
} | |||
template <typename Iterator> | |||
static bool skipNumberSuffix (Iterator& source) | |||
{ | |||
const juce_wchar c = source.peekNextChar(); | |||
if (c == 'l' || c == 'L' || c == 'u' || c == 'U') | |||
source.skip(); | |||
if (CharacterFunctions::isLetterOrDigit (source.peekNextChar())) | |||
return false; | |||
return true; | |||
} | |||
static bool isHexDigit (const juce_wchar c) noexcept | |||
{ | |||
return (c >= '0' && c <= '9') | |||
|| (c >= 'a' && c <= 'f') | |||
|| (c >= 'A' && c <= 'F'); | |||
} | |||
template <typename Iterator> | |||
static bool parseHexLiteral (Iterator& source) noexcept | |||
{ | |||
if (source.peekNextChar() == '-') | |||
source.skip(); | |||
if (source.nextChar() != '0') | |||
return false; | |||
juce_wchar c = source.nextChar(); | |||
if (c != 'x' && c != 'X') | |||
return false; | |||
int numDigits = 0; | |||
while (isHexDigit (source.peekNextChar())) | |||
{ | |||
++numDigits; | |||
source.skip(); | |||
} | |||
if (numDigits == 0) | |||
return false; | |||
return skipNumberSuffix (source); | |||
} | |||
static bool isOctalDigit (const juce_wchar c) noexcept | |||
{ | |||
return c >= '0' && c <= '7'; | |||
} | |||
template <typename Iterator> | |||
static bool parseOctalLiteral (Iterator& source) noexcept | |||
{ | |||
if (source.peekNextChar() == '-') | |||
source.skip(); | |||
if (source.nextChar() != '0') | |||
return false; | |||
if (! isOctalDigit (source.nextChar())) | |||
return false; | |||
while (isOctalDigit (source.peekNextChar())) | |||
source.skip(); | |||
return skipNumberSuffix (source); | |||
} | |||
static bool isDecimalDigit (const juce_wchar c) noexcept | |||
{ | |||
return c >= '0' && c <= '9'; | |||
} | |||
template <typename Iterator> | |||
static bool parseDecimalLiteral (Iterator& source) noexcept | |||
{ | |||
if (source.peekNextChar() == '-') | |||
source.skip(); | |||
int numChars = 0; | |||
while (isDecimalDigit (source.peekNextChar())) | |||
{ | |||
++numChars; | |||
source.skip(); | |||
} | |||
if (numChars == 0) | |||
return false; | |||
return skipNumberSuffix (source); | |||
} | |||
template <typename Iterator> | |||
static bool parseFloatLiteral (Iterator& source) noexcept | |||
{ | |||
if (source.peekNextChar() == '-') | |||
source.skip(); | |||
int numDigits = 0; | |||
while (isDecimalDigit (source.peekNextChar())) | |||
{ | |||
source.skip(); | |||
++numDigits; | |||
} | |||
const bool hasPoint = (source.peekNextChar() == '.'); | |||
if (hasPoint) | |||
{ | |||
source.skip(); | |||
while (isDecimalDigit (source.peekNextChar())) | |||
{ | |||
source.skip(); | |||
++numDigits; | |||
} | |||
} | |||
if (numDigits == 0) | |||
return false; | |||
juce_wchar c = source.peekNextChar(); | |||
const bool hasExponent = (c == 'e' || c == 'E'); | |||
if (hasExponent) | |||
{ | |||
source.skip(); | |||
c = source.peekNextChar(); | |||
if (c == '+' || c == '-') | |||
source.skip(); | |||
int numExpDigits = 0; | |||
while (isDecimalDigit (source.peekNextChar())) | |||
{ | |||
source.skip(); | |||
++numExpDigits; | |||
} | |||
if (numExpDigits == 0) | |||
return false; | |||
} | |||
c = source.peekNextChar(); | |||
if (c == 'f' || c == 'F') | |||
source.skip(); | |||
else if (! (hasExponent || hasPoint)) | |||
return false; | |||
return true; | |||
} | |||
template <typename Iterator> | |||
static int parseNumber (Iterator& source) | |||
{ | |||
const Iterator original (source); | |||
if (parseFloatLiteral (source)) return CPlusPlusCodeTokeniser::tokenType_float; | |||
source = original; | |||
if (parseHexLiteral (source)) return CPlusPlusCodeTokeniser::tokenType_integer; | |||
source = original; | |||
if (parseOctalLiteral (source)) return CPlusPlusCodeTokeniser::tokenType_integer; | |||
source = original; | |||
if (parseDecimalLiteral (source)) return CPlusPlusCodeTokeniser::tokenType_integer; | |||
source = original; | |||
return CPlusPlusCodeTokeniser::tokenType_error; | |||
} | |||
template <typename Iterator> | |||
static void skipQuotedString (Iterator& source) noexcept | |||
{ | |||
const juce_wchar quote = source.nextChar(); | |||
for (;;) | |||
{ | |||
const juce_wchar c = source.nextChar(); | |||
if (c == quote || c == 0) | |||
break; | |||
if (c == '\\') | |||
source.skip(); | |||
} | |||
} | |||
template <typename Iterator> | |||
static void skipComment (Iterator& source) noexcept | |||
{ | |||
bool lastWasStar = false; | |||
for (;;) | |||
{ | |||
const juce_wchar c = source.nextChar(); | |||
if (c == 0 || (c == '/' && lastWasStar)) | |||
break; | |||
lastWasStar = (c == '*'); | |||
} | |||
} | |||
template <typename Iterator> | |||
static void skipPreprocessorLine (Iterator& source) noexcept | |||
{ | |||
bool lastWasBackslash = false; | |||
for (;;) | |||
{ | |||
const juce_wchar c = source.peekNextChar(); | |||
if (c == '"') | |||
{ | |||
skipQuotedString (source); | |||
continue; | |||
} | |||
if (c == '/') | |||
{ | |||
Iterator next (source); | |||
next.skip(); | |||
const juce_wchar c2 = next.peekNextChar(); | |||
if (c2 == '/' || c2 == '*') | |||
return; | |||
} | |||
if (c == 0) | |||
break; | |||
if (c == '\n' || c == '\r') | |||
{ | |||
source.skipToEndOfLine(); | |||
if (lastWasBackslash) | |||
skipPreprocessorLine (source); | |||
break; | |||
} | |||
lastWasBackslash = (c == '\\'); | |||
source.skip(); | |||
} | |||
} | |||
template <typename Iterator> | |||
static void skipIfNextCharMatches (Iterator& source, const juce_wchar c) noexcept | |||
{ | |||
if (source.peekNextChar() == c) | |||
source.skip(); | |||
} | |||
template <typename Iterator> | |||
static void skipIfNextCharMatches (Iterator& source, const juce_wchar c1, const juce_wchar c2) noexcept | |||
{ | |||
const juce_wchar c = source.peekNextChar(); | |||
if (c == c1 || c == c2) | |||
source.skip(); | |||
} | |||
template <typename Iterator> | |||
static int readNextToken (Iterator& source) | |||
{ | |||
source.skipWhitespace(); | |||
const juce_wchar firstChar = source.peekNextChar(); | |||
switch (firstChar) | |||
{ | |||
case 0: | |||
break; | |||
case '0': case '1': case '2': case '3': case '4': | |||
case '5': case '6': case '7': case '8': case '9': | |||
case '.': | |||
{ | |||
int result = parseNumber (source); | |||
if (result == CPlusPlusCodeTokeniser::tokenType_error) | |||
{ | |||
source.skip(); | |||
if (firstChar == '.') | |||
return CPlusPlusCodeTokeniser::tokenType_punctuation; | |||
} | |||
return result; | |||
} | |||
case ',': | |||
case ';': | |||
case ':': | |||
source.skip(); | |||
return CPlusPlusCodeTokeniser::tokenType_punctuation; | |||
case '(': case ')': | |||
case '{': case '}': | |||
case '[': case ']': | |||
source.skip(); | |||
return CPlusPlusCodeTokeniser::tokenType_bracket; | |||
case '"': | |||
case '\'': | |||
skipQuotedString (source); | |||
return CPlusPlusCodeTokeniser::tokenType_string; | |||
case '+': | |||
source.skip(); | |||
skipIfNextCharMatches (source, '+', '='); | |||
return CPlusPlusCodeTokeniser::tokenType_operator; | |||
case '-': | |||
{ | |||
source.skip(); | |||
int result = parseNumber (source); | |||
if (result == CPlusPlusCodeTokeniser::tokenType_error) | |||
{ | |||
skipIfNextCharMatches (source, '-', '='); | |||
return CPlusPlusCodeTokeniser::tokenType_operator; | |||
} | |||
return result; | |||
} | |||
case '*': case '%': | |||
case '=': case '!': | |||
source.skip(); | |||
skipIfNextCharMatches (source, '='); | |||
return CPlusPlusCodeTokeniser::tokenType_operator; | |||
case '/': | |||
{ | |||
source.skip(); | |||
juce_wchar nextChar = source.peekNextChar(); | |||
if (nextChar == '/') | |||
{ | |||
source.skipToEndOfLine(); | |||
return CPlusPlusCodeTokeniser::tokenType_comment; | |||
} | |||
if (nextChar == '*') | |||
{ | |||
source.skip(); | |||
skipComment (source); | |||
return CPlusPlusCodeTokeniser::tokenType_comment; | |||
} | |||
if (nextChar == '=') | |||
source.skip(); | |||
return CPlusPlusCodeTokeniser::tokenType_operator; | |||
} | |||
case '?': | |||
case '~': | |||
source.skip(); | |||
return CPlusPlusCodeTokeniser::tokenType_operator; | |||
case '<': case '>': | |||
case '|': case '&': case '^': | |||
source.skip(); | |||
skipIfNextCharMatches (source, firstChar); | |||
skipIfNextCharMatches (source, '='); | |||
return CPlusPlusCodeTokeniser::tokenType_operator; | |||
case '#': | |||
skipPreprocessorLine (source); | |||
return CPlusPlusCodeTokeniser::tokenType_preprocessor; | |||
default: | |||
if (isIdentifierStart (firstChar)) | |||
return parseIdentifier (source); | |||
source.skip(); | |||
break; | |||
} | |||
return CPlusPlusCodeTokeniser::tokenType_error; | |||
} | |||
/** A class that can be passed to the CppTokeniserFunctions functions in order to | |||
parse a String. | |||
*/ | |||
struct StringIterator | |||
{ | |||
StringIterator (const String& s) noexcept : t (s.getCharPointer()), numChars (0) {} | |||
StringIterator (String::CharPointerType s) noexcept : t (s), numChars (0) {} | |||
juce_wchar nextChar() noexcept { if (isEOF()) return 0; ++numChars; return t.getAndAdvance(); } | |||
juce_wchar peekNextChar()noexcept { return *t; } | |||
void skip() noexcept { if (! isEOF()) { ++t; ++numChars; } } | |||
void skipWhitespace() noexcept { while (t.isWhitespace()) skip(); } | |||
void skipToEndOfLine() noexcept { while (*t != '\r' && *t != '\n' && *t != 0) skip(); } | |||
bool isEOF() const noexcept { return t.isEmpty(); } | |||
String::CharPointerType t; | |||
int numChars; | |||
}; | |||
//============================================================================== | |||
/** Takes a UTF8 string and writes it to a stream using standard C++ escape sequences for any | |||
non-ascii bytes. | |||
Although not strictly a tokenising function, this is still a function that often comes in | |||
handy when working with C++ code! | |||
Note that addEscapeChars() is easier to use than this function if you're working with Strings. | |||
@see addEscapeChars | |||
*/ | |||
static void writeEscapeChars (OutputStream& out, const char* utf8, const int numBytesToRead, | |||
const int maxCharsOnLine, const bool breakAtNewLines, | |||
const bool replaceSingleQuotes, const bool allowStringBreaks) | |||
{ | |||
int charsOnLine = 0; | |||
bool lastWasHexEscapeCode = false; | |||
bool trigraphDetected = false; | |||
for (int i = 0; i < numBytesToRead || numBytesToRead < 0; ++i) | |||
{ | |||
const unsigned char c = (unsigned char) utf8[i]; | |||
bool startNewLine = false; | |||
switch (c) | |||
{ | |||
case '\t': out << "\\t"; trigraphDetected = false; lastWasHexEscapeCode = false; charsOnLine += 2; break; | |||
case '\r': out << "\\r"; trigraphDetected = false; lastWasHexEscapeCode = false; charsOnLine += 2; break; | |||
case '\n': out << "\\n"; trigraphDetected = false; lastWasHexEscapeCode = false; charsOnLine += 2; startNewLine = breakAtNewLines; break; | |||
case '\\': out << "\\\\"; trigraphDetected = false; lastWasHexEscapeCode = false; charsOnLine += 2; break; | |||
case '\"': out << "\\\""; trigraphDetected = false; lastWasHexEscapeCode = false; charsOnLine += 2; break; | |||
case '?': | |||
if (trigraphDetected) | |||
{ | |||
out << "\\?"; | |||
charsOnLine++; | |||
trigraphDetected = false; | |||
} | |||
else | |||
{ | |||
out << "?"; | |||
trigraphDetected = true; | |||
} | |||
lastWasHexEscapeCode = false; | |||
charsOnLine++; | |||
break; | |||
case 0: | |||
if (numBytesToRead < 0) | |||
return; | |||
out << "\\0"; | |||
lastWasHexEscapeCode = true; | |||
trigraphDetected = false; | |||
charsOnLine += 2; | |||
break; | |||
case '\'': | |||
if (replaceSingleQuotes) | |||
{ | |||
out << "\\\'"; | |||
lastWasHexEscapeCode = false; | |||
trigraphDetected = false; | |||
charsOnLine += 2; | |||
break; | |||
} | |||
// deliberate fall-through... | |||
default: | |||
if (c >= 32 && c < 127 && ! (lastWasHexEscapeCode // (have to avoid following a hex escape sequence with a valid hex digit) | |||
&& CharacterFunctions::getHexDigitValue (c) >= 0)) | |||
{ | |||
out << (char) c; | |||
lastWasHexEscapeCode = false; | |||
trigraphDetected = false; | |||
++charsOnLine; | |||
} | |||
else if (allowStringBreaks && lastWasHexEscapeCode && c >= 32 && c < 127) | |||
{ | |||
out << "\"\"" << (char) c; | |||
lastWasHexEscapeCode = false; | |||
trigraphDetected = false; | |||
charsOnLine += 3; | |||
} | |||
else | |||
{ | |||
out << (c < 16 ? "\\x0" : "\\x") << String::toHexString ((int) c); | |||
lastWasHexEscapeCode = true; | |||
trigraphDetected = false; | |||
charsOnLine += 4; | |||
} | |||
break; | |||
} | |||
if ((startNewLine || (maxCharsOnLine > 0 && charsOnLine >= maxCharsOnLine)) | |||
&& (numBytesToRead < 0 || i < numBytesToRead - 1)) | |||
{ | |||
charsOnLine = 0; | |||
out << "\"" << newLine << "\""; | |||
lastWasHexEscapeCode = false; | |||
} | |||
} | |||
} | |||
/** Takes a string and returns a version of it where standard C++ escape sequences have been | |||
used to replace any non-ascii bytes. | |||
Although not strictly a tokenising function, this is still a function that often comes in | |||
handy when working with C++ code! | |||
@see writeEscapeChars | |||
*/ | |||
static String addEscapeChars (const String& s) | |||
{ | |||
MemoryOutputStream mo; | |||
writeEscapeChars (mo, s.toRawUTF8(), -1, -1, false, true, true); | |||
return mo.toString(); | |||
} | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,418 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class CodeDocumentLine; | |||
//============================================================================== | |||
/** | |||
A class for storing and manipulating a source code file. | |||
When using a CodeEditorComponent, it takes one of these as its source object. | |||
The CodeDocument stores its content as an array of lines, which makes it | |||
quick to insert and delete. | |||
@see CodeEditorComponent | |||
*/ | |||
class JUCE_API CodeDocument | |||
{ | |||
public: | |||
/** Creates a new, empty document. */ | |||
CodeDocument(); | |||
/** Destructor. */ | |||
~CodeDocument(); | |||
//============================================================================== | |||
/** A position in a code document. | |||
Using this class you can find a position in a code document and quickly get its | |||
character position, line, and index. By calling setPositionMaintained (true), the | |||
position is automatically updated when text is inserted or deleted in the document, | |||
so that it maintains its original place in the text. | |||
*/ | |||
class JUCE_API Position | |||
{ | |||
public: | |||
/** Creates an uninitialised position. | |||
Don't attempt to call any methods on this until you've given it an owner document | |||
to refer to! | |||
*/ | |||
Position() noexcept; | |||
/** Creates a position based on a line and index in a document. | |||
Note that this index is NOT the column number, it's the number of characters from the | |||
start of the line. The "column" number isn't quite the same, because if the line | |||
contains any tab characters, the relationship of the index to its visual column depends on | |||
the number of spaces per tab being used! | |||
Lines are numbered from zero, and if the line or index are beyond the bounds of the document, | |||
they will be adjusted to keep them within its limits. | |||
*/ | |||
Position (const CodeDocument& ownerDocument, | |||
int line, int indexInLine) noexcept; | |||
/** Creates a position based on a character index in a document. | |||
This position is placed at the specified number of characters from the start of the | |||
document. The line and column are auto-calculated. | |||
If the position is beyond the range of the document, it'll be adjusted to keep it | |||
inside. | |||
*/ | |||
Position (const CodeDocument& ownerDocument, | |||
int charactersFromStartOfDocument) noexcept; | |||
/** Creates a copy of another position. | |||
This will copy the position, but the new object will not be set to maintain its position, | |||
even if the source object was set to do so. | |||
*/ | |||
Position (const Position&) noexcept; | |||
/** Destructor. */ | |||
~Position(); | |||
Position& operator= (const Position&); | |||
bool operator== (const Position&) const noexcept; | |||
bool operator!= (const Position&) const noexcept; | |||
/** Points this object at a new position within the document. | |||
If the position is beyond the range of the document, it'll be adjusted to keep it | |||
inside. | |||
@see getPosition, setLineAndIndex | |||
*/ | |||
void setPosition (int charactersFromStartOfDocument); | |||
/** Returns the position as the number of characters from the start of the document. | |||
@see setPosition, getLineNumber, getIndexInLine | |||
*/ | |||
int getPosition() const noexcept { return characterPos; } | |||
/** Moves the position to a new line and index within the line. | |||
Note that the index is NOT the column at which the position appears in an editor. | |||
If the line contains any tab characters, the relationship of the index to its | |||
visual position depends on the number of spaces per tab being used! | |||
Lines are numbered from zero, and if the line or index are beyond the bounds of the document, | |||
they will be adjusted to keep them within its limits. | |||
*/ | |||
void setLineAndIndex (int newLineNumber, int newIndexInLine); | |||
/** Returns the line number of this position. | |||
The first line in the document is numbered zero, not one! | |||
*/ | |||
int getLineNumber() const noexcept { return line; } | |||
/** Returns the number of characters from the start of the line. | |||
Note that this value is NOT the column at which the position appears in an editor. | |||
If the line contains any tab characters, the relationship of the index to its | |||
visual position depends on the number of spaces per tab being used! | |||
*/ | |||
int getIndexInLine() const noexcept { return indexInLine; } | |||
/** Allows the position to be automatically updated when the document changes. | |||
If this is set to true, the position will register with its document so that | |||
when the document has text inserted or deleted, this position will be automatically | |||
moved to keep it at the same position in the text. | |||
*/ | |||
void setPositionMaintained (bool isMaintained); | |||
//============================================================================== | |||
/** Moves the position forwards or backwards by the specified number of characters. | |||
@see movedBy | |||
*/ | |||
void moveBy (int characterDelta); | |||
/** Returns a position which is the same as this one, moved by the specified number of | |||
characters. | |||
@see moveBy | |||
*/ | |||
Position movedBy (int characterDelta) const; | |||
/** Returns a position which is the same as this one, moved up or down by the specified | |||
number of lines. | |||
@see movedBy | |||
*/ | |||
Position movedByLines (int deltaLines) const; | |||
/** Returns the character in the document at this position. | |||
@see getLineText | |||
*/ | |||
juce_wchar getCharacter() const; | |||
/** Returns the line from the document that this position is within. | |||
@see getCharacter, getLineNumber | |||
*/ | |||
String getLineText() const; | |||
private: | |||
CodeDocument* owner; | |||
int characterPos, line, indexInLine; | |||
bool positionMaintained; | |||
}; | |||
//============================================================================== | |||
/** Returns the full text of the document. */ | |||
String getAllContent() const; | |||
/** Returns a section of the document's text. */ | |||
String getTextBetween (const Position& start, const Position& end) const; | |||
/** Returns a line from the document. */ | |||
String getLine (int lineIndex) const noexcept; | |||
/** Returns the number of characters in the document. */ | |||
int getNumCharacters() const noexcept; | |||
/** Returns the number of lines in the document. */ | |||
int getNumLines() const noexcept { return lines.size(); } | |||
/** Returns the number of characters in the longest line of the document. */ | |||
int getMaximumLineLength() noexcept; | |||
/** Deletes a section of the text. | |||
This operation is undoable. | |||
*/ | |||
void deleteSection (const Position& startPosition, const Position& endPosition); | |||
/** Deletes a section of the text. | |||
This operation is undoable. | |||
*/ | |||
void deleteSection (int startIndex, int endIndex); | |||
/** Inserts some text into the document at a given position. | |||
This operation is undoable. | |||
*/ | |||
void insertText (const Position& position, const String& text); | |||
/** Inserts some text into the document at a given position. | |||
This operation is undoable. | |||
*/ | |||
void insertText (int insertIndex, const String& text); | |||
/** Replaces a section of the text with a new string. | |||
This operation is undoable. | |||
*/ | |||
void replaceSection (int startIndex, int endIndex, const String& newText); | |||
/** Clears the document and replaces it with some new text. | |||
This operation is undoable - if you're trying to completely reset the document, you | |||
might want to also call clearUndoHistory() and setSavePoint() after using this method. | |||
*/ | |||
void replaceAllContent (const String& newContent); | |||
/** Analyses the changes between the current content and some new text, and applies | |||
those changes. | |||
*/ | |||
void applyChanges (const String& newContent); | |||
/** Replaces the editor's contents with the contents of a stream. | |||
This will also reset the undo history and save point marker. | |||
*/ | |||
bool loadFromStream (InputStream& stream); | |||
/** Writes the editor's current contents to a stream. */ | |||
bool writeToStream (OutputStream& stream); | |||
//============================================================================== | |||
/** Returns the preferred new-line characters for the document. | |||
This will be either "\n", "\r\n", or (rarely) "\r". | |||
@see setNewLineCharacters | |||
*/ | |||
String getNewLineCharacters() const noexcept { return newLineChars; } | |||
/** Sets the new-line characters that the document should use. | |||
The string must be either "\n", "\r\n", or (rarely) "\r". | |||
@see getNewLineCharacters | |||
*/ | |||
void setNewLineCharacters (const String& newLineCharacters) noexcept; | |||
//============================================================================== | |||
/** Begins a new undo transaction. | |||
The document itself will not call this internally, so relies on whatever is using the | |||
document to periodically call this to break up the undo sequence into sensible chunks. | |||
@see UndoManager::beginNewTransaction | |||
*/ | |||
void newTransaction(); | |||
/** Undo the last operation. | |||
@see UndoManager::undo | |||
*/ | |||
void undo(); | |||
/** Redo the last operation. | |||
@see UndoManager::redo | |||
*/ | |||
void redo(); | |||
/** Clears the undo history. | |||
@see UndoManager::clearUndoHistory | |||
*/ | |||
void clearUndoHistory(); | |||
/** Returns the document's UndoManager */ | |||
UndoManager& getUndoManager() noexcept { return undoManager; } | |||
//============================================================================== | |||
/** Makes a note that the document's current state matches the one that is saved. | |||
After this has been called, hasChangedSinceSavePoint() will return false until | |||
the document has been altered, and then it'll start returning true. If the document is | |||
altered, but then undone until it gets back to this state, hasChangedSinceSavePoint() | |||
will again return false. | |||
@see hasChangedSinceSavePoint | |||
*/ | |||
void setSavePoint() noexcept; | |||
/** Returns true if the state of the document differs from the state it was in when | |||
setSavePoint() was last called. | |||
@see setSavePoint | |||
*/ | |||
bool hasChangedSinceSavePoint() const noexcept; | |||
//============================================================================== | |||
/** Searches for a word-break. */ | |||
Position findWordBreakAfter (const Position& position) const noexcept; | |||
/** Searches for a word-break. */ | |||
Position findWordBreakBefore (const Position& position) const noexcept; | |||
/** Finds the token that contains the given position. */ | |||
void findTokenContaining (const Position& pos, Position& start, Position& end) const noexcept; | |||
/** Finds the line that contains the given position. */ | |||
void findLineContaining (const Position& pos, Position& start, Position& end) const noexcept; | |||
//============================================================================== | |||
/** An object that receives callbacks from the CodeDocument when its text changes. | |||
@see CodeDocument::addListener, CodeDocument::removeListener | |||
*/ | |||
class JUCE_API Listener | |||
{ | |||
public: | |||
Listener() {} | |||
virtual ~Listener() {} | |||
/** Called by a CodeDocument when text is added. */ | |||
virtual void codeDocumentTextInserted (const String& newText, int insertIndex) = 0; | |||
/** Called by a CodeDocument when text is deleted. */ | |||
virtual void codeDocumentTextDeleted (int startIndex, int endIndex) = 0; | |||
}; | |||
/** Registers a listener object to receive callbacks when the document changes. | |||
If the listener is already registered, this method has no effect. | |||
@see removeListener | |||
*/ | |||
void addListener (Listener* listener) noexcept; | |||
/** Deregisters a listener. | |||
@see addListener | |||
*/ | |||
void removeListener (Listener* listener) noexcept; | |||
//============================================================================== | |||
/** Iterates the text in a CodeDocument. | |||
This class lets you read characters from a CodeDocument. It's designed to be used | |||
by a CodeTokeniser object. | |||
@see CodeDocument | |||
*/ | |||
class JUCE_API Iterator | |||
{ | |||
public: | |||
Iterator (const CodeDocument& document) noexcept; | |||
Iterator (const Iterator&) noexcept; | |||
Iterator& operator= (const Iterator&) noexcept; | |||
~Iterator() noexcept; | |||
/** Reads the next character and returns it. | |||
@see peekNextChar | |||
*/ | |||
juce_wchar nextChar() noexcept; | |||
/** Reads the next character without advancing the current position. */ | |||
juce_wchar peekNextChar() const noexcept; | |||
/** Advances the position by one character. */ | |||
void skip() noexcept; | |||
/** Returns the position as the number of characters from the start of the document. */ | |||
int getPosition() const noexcept { return position; } | |||
/** Skips over any whitespace characters until the next character is non-whitespace. */ | |||
void skipWhitespace() noexcept; | |||
/** Skips forward until the next character will be the first character on the next line */ | |||
void skipToEndOfLine() noexcept; | |||
/** Returns the line number of the next character. */ | |||
int getLine() const noexcept { return line; } | |||
/** Returns true if the iterator has reached the end of the document. */ | |||
bool isEOF() const noexcept; | |||
private: | |||
const CodeDocument* document; | |||
mutable String::CharPointerType charPointer; | |||
int line, position; | |||
}; | |||
private: | |||
//============================================================================== | |||
friend class CodeDocumentInsertAction; | |||
friend class CodeDocumentDeleteAction; | |||
friend class Iterator; | |||
friend class Position; | |||
OwnedArray <CodeDocumentLine> lines; | |||
Array <Position*> positionsToMaintain; | |||
UndoManager undoManager; | |||
int currentActionIndex, indexOfSavedState; | |||
int maximumLineLength; | |||
ListenerList <Listener> listeners; | |||
String newLineChars; | |||
void insert (const String& text, int insertPos, bool undoable); | |||
void remove (int startPos, int endPos, bool undoable); | |||
void checkLastLineStatus(); | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CodeDocument) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,435 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class CodeTokeniser; | |||
//============================================================================== | |||
/** | |||
A text editor component designed specifically for source code. | |||
This is designed to handle syntax highlighting and fast editing of very large | |||
files. | |||
*/ | |||
class JUCE_API CodeEditorComponent : public Component, | |||
public ApplicationCommandTarget, | |||
public TextInputTarget | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates an editor for a document. | |||
The tokeniser object is optional - pass nullptr to disable syntax highlighting. | |||
The object that you pass in is not owned or deleted by the editor - you must | |||
make sure that it doesn't get deleted while this component is still using it. | |||
@see CodeDocument | |||
*/ | |||
CodeEditorComponent (CodeDocument& document, | |||
CodeTokeniser* codeTokeniser); | |||
/** Destructor. */ | |||
~CodeEditorComponent(); | |||
//============================================================================== | |||
/** Returns the code document that this component is editing. */ | |||
CodeDocument& getDocument() const noexcept { return document; } | |||
/** Loads the given content into the document. | |||
This will completely reset the CodeDocument object, clear its undo history, | |||
and fill it with this text. | |||
*/ | |||
void loadContent (const String& newContent); | |||
//============================================================================== | |||
/** Returns the standard character width. */ | |||
float getCharWidth() const noexcept { return charWidth; } | |||
/** Returns the height of a line of text, in pixels. */ | |||
int getLineHeight() const noexcept { return lineHeight; } | |||
/** Returns the number of whole lines visible on the screen, | |||
This doesn't include a cut-off line that might be visible at the bottom if the | |||
component's height isn't an exact multiple of the line-height. | |||
*/ | |||
int getNumLinesOnScreen() const noexcept { return linesOnScreen; } | |||
/** Returns the index of the first line that's visible at the top of the editor. */ | |||
int getFirstLineOnScreen() const noexcept { return firstLineOnScreen; } | |||
/** Returns the number of whole columns visible on the screen. | |||
This doesn't include any cut-off columns at the right-hand edge. | |||
*/ | |||
int getNumColumnsOnScreen() const noexcept { return columnsOnScreen; } | |||
/** Returns the current caret position. */ | |||
CodeDocument::Position getCaretPos() const { return caretPos; } | |||
/** Returns the position of the caret, relative to the editor's origin. */ | |||
Rectangle<int> getCaretRectangle() override; | |||
/** Moves the caret. | |||
If selecting is true, the section of the document between the current | |||
caret position and the new one will become selected. If false, any currently | |||
selected region will be deselected. | |||
*/ | |||
void moveCaretTo (const CodeDocument::Position& newPos, bool selecting); | |||
/** Returns the on-screen position of a character in the document. | |||
The rectangle returned is relative to this component's top-left origin. | |||
*/ | |||
Rectangle<int> getCharacterBounds (const CodeDocument::Position& pos) const; | |||
/** Finds the character at a given on-screen position. | |||
The coordinates are relative to this component's top-left origin. | |||
*/ | |||
CodeDocument::Position getPositionAt (int x, int y); | |||
/** Returns the start of the selection as a position. */ | |||
CodeDocument::Position getSelectionStart() const { return selectionStart; } | |||
/** Returns the end of the selection as a position. */ | |||
CodeDocument::Position getSelectionEnd() const { return selectionEnd; } | |||
/** Enables or disables the line-number display in the gutter. */ | |||
void setLineNumbersShown (bool shouldBeShown); | |||
//============================================================================== | |||
bool moveCaretLeft (bool moveInWholeWordSteps, bool selecting); | |||
bool moveCaretRight (bool moveInWholeWordSteps, bool selecting); | |||
bool moveCaretUp (bool selecting); | |||
bool moveCaretDown (bool selecting); | |||
bool scrollDown(); | |||
bool scrollUp(); | |||
bool pageUp (bool selecting); | |||
bool pageDown (bool selecting); | |||
bool moveCaretToTop (bool selecting); | |||
bool moveCaretToStartOfLine (bool selecting); | |||
bool moveCaretToEnd (bool selecting); | |||
bool moveCaretToEndOfLine (bool selecting); | |||
bool deleteBackwards (bool moveInWholeWordSteps); | |||
bool deleteForwards (bool moveInWholeWordSteps); | |||
bool deleteWhitespaceBackwardsToTabStop(); | |||
virtual bool copyToClipboard(); | |||
virtual bool cutToClipboard(); | |||
virtual bool pasteFromClipboard(); | |||
bool undo(); | |||
bool redo(); | |||
void selectRegion (const CodeDocument::Position& start, const CodeDocument::Position& end); | |||
bool selectAll(); | |||
void deselectAll(); | |||
void scrollToLine (int newFirstLineOnScreen); | |||
void scrollBy (int deltaLines); | |||
void scrollToColumn (int newFirstColumnOnScreen); | |||
void scrollToKeepCaretOnScreen(); | |||
void scrollToKeepLinesOnScreen (Range<int> linesToShow); | |||
void insertTextAtCaret (const String& textToInsert) override; | |||
void insertTabAtCaret(); | |||
void indentSelection(); | |||
void unindentSelection(); | |||
//============================================================================== | |||
Range<int> getHighlightedRegion() const override; | |||
bool isHighlightActive() const noexcept; | |||
void setHighlightedRegion (const Range<int>& newRange) override; | |||
String getTextInRange (const Range<int>& range) const override; | |||
//============================================================================== | |||
/** Can be used to save and restore the editor's caret position, selection state, etc. */ | |||
struct State | |||
{ | |||
/** Creates an object containing the state of the given editor. */ | |||
State (const CodeEditorComponent&); | |||
/** Creates a state object from a string that was previously created with toString(). */ | |||
State (const String& stringifiedVersion); | |||
State (const State&) noexcept; | |||
/** Updates the given editor with this saved state. */ | |||
void restoreState (CodeEditorComponent&) const; | |||
/** Returns a stringified version of this state that can be used to recreate it later. */ | |||
String toString() const; | |||
private: | |||
int lastTopLine, lastCaretPos, lastSelectionEnd; | |||
}; | |||
//============================================================================== | |||
/** Changes the current tab settings. | |||
This lets you change the tab size and whether pressing the tab key inserts a | |||
tab character, or its equivalent number of spaces. | |||
*/ | |||
void setTabSize (int numSpacesPerTab, bool insertSpacesInsteadOfTabCharacters); | |||
/** Returns the current number of spaces per tab. | |||
@see setTabSize | |||
*/ | |||
int getTabSize() const noexcept { return spacesPerTab; } | |||
/** Returns true if the tab key will insert spaces instead of actual tab characters. | |||
@see setTabSize | |||
*/ | |||
bool areSpacesInsertedForTabs() const { return useSpacesForTabs; } | |||
/** Returns a string containing spaces or tab characters to generate the given number of spaces. */ | |||
String getTabString (int numSpaces) const; | |||
/** Changes the font. | |||
Make sure you only use a fixed-width font, or this component will look pretty nasty! | |||
*/ | |||
void setFont (const Font& newFont); | |||
/** Returns the font that the editor is using. */ | |||
const Font& getFont() const noexcept { return font; } | |||
/** Makes the editor read-only. */ | |||
void setReadOnly (bool shouldBeReadOnly) noexcept; | |||
/** Returns true if the editor is set to be read-only. */ | |||
bool isReadOnly() const noexcept { return readOnly; } | |||
//============================================================================== | |||
struct JUCE_API ColourScheme | |||
{ | |||
struct TokenType | |||
{ | |||
String name; | |||
Colour colour; | |||
}; | |||
Array<TokenType> types; | |||
void set (const String& name, Colour colour); | |||
}; | |||
/** Changes the syntax highlighting scheme. | |||
The token type values are dependent on the tokeniser being used - use | |||
CodeTokeniser::getTokenTypes() to get a list of the token types. | |||
@see getColourForTokenType | |||
*/ | |||
void setColourScheme (const ColourScheme& scheme); | |||
/** Returns the current syntax highlighting colour scheme. */ | |||
const ColourScheme& getColourScheme() const noexcept { return colourScheme; } | |||
/** Returns one the syntax highlighting colour for the given token. | |||
The token type values are dependent on the tokeniser being used. | |||
@see setColourScheme | |||
*/ | |||
Colour getColourForTokenType (int tokenType) const; | |||
//============================================================================== | |||
/** A set of colour IDs to use to change the colour of various aspects of the editor. | |||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() | |||
methods. | |||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour | |||
*/ | |||
enum ColourIds | |||
{ | |||
backgroundColourId = 0x1004500, /**< A colour to use to fill the editor's background. */ | |||
highlightColourId = 0x1004502, /**< The colour to use for the highlighted background under selected text. */ | |||
defaultTextColourId = 0x1004503, /**< The colour to use for text when no syntax colouring is enabled. */ | |||
lineNumberBackgroundId = 0x1004504, /**< The colour to use for filling the background of the line-number gutter. */ | |||
lineNumberTextId = 0x1004505, /**< The colour to use for drawing the line numbers. */ | |||
}; | |||
//============================================================================== | |||
/** Changes the size of the scrollbars. */ | |||
void setScrollbarThickness (int thickness); | |||
/** Returns the thickness of the scrollbars. */ | |||
int getScrollbarThickness() const noexcept { return scrollbarThickness; } | |||
//============================================================================== | |||
/** Called when the return key is pressed - this can be overridden for custom behaviour. */ | |||
virtual void handleReturnKey(); | |||
/** Called when the tab key is pressed - this can be overridden for custom behaviour. */ | |||
virtual void handleTabKey(); | |||
/** Called when the escape key is pressed - this can be overridden for custom behaviour. */ | |||
virtual void handleEscapeKey(); | |||
/** Called when the view position is scrolled horizontally or vertically. */ | |||
virtual void editorViewportPositionChanged(); | |||
//============================================================================== | |||
/** This adds the items to the popup menu. | |||
By default it adds the cut/copy/paste items, but you can override this if | |||
you need to replace these with your own items. | |||
If you want to add your own items to the existing ones, you can override this, | |||
call the base class's addPopupMenuItems() method, then append your own items. | |||
When the menu has been shown, performPopupMenuAction() will be called to | |||
perform the item that the user has chosen. | |||
If this was triggered by a mouse-click, the mouseClickEvent parameter will be | |||
a pointer to the info about it, or may be null if the menu is being triggered | |||
by some other means. | |||
@see performPopupMenuAction, setPopupMenuEnabled, isPopupMenuEnabled | |||
*/ | |||
virtual void addPopupMenuItems (PopupMenu& menuToAddTo, | |||
const MouseEvent* mouseClickEvent); | |||
/** This is called to perform one of the items that was shown on the popup menu. | |||
If you've overridden addPopupMenuItems(), you should also override this | |||
to perform the actions that you've added. | |||
If you've overridden addPopupMenuItems() but have still left the default items | |||
on the menu, remember to call the superclass's performPopupMenuAction() | |||
so that it can perform the default actions if that's what the user clicked on. | |||
@see addPopupMenuItems, setPopupMenuEnabled, isPopupMenuEnabled | |||
*/ | |||
virtual void performPopupMenuAction (int menuItemID); | |||
/** Specifies a commmand-manager which the editor will notify whenever the state | |||
of any of its commands changes. | |||
If you're making use of the editor's ApplicationCommandTarget interface, then | |||
you should also use this to tell it which command manager it should use. Make | |||
sure that the manager does not go out of scope while the editor is using it. You | |||
can pass a nullptr here to disable this. | |||
*/ | |||
void setCommandManager (ApplicationCommandManager* newManager) noexcept; | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
void resized() override; | |||
/** @internal */ | |||
bool keyPressed (const KeyPress&) override; | |||
/** @internal */ | |||
void mouseDown (const MouseEvent&) override; | |||
/** @internal */ | |||
void mouseDrag (const MouseEvent&) override; | |||
/** @internal */ | |||
void mouseUp (const MouseEvent&) override; | |||
/** @internal */ | |||
void mouseDoubleClick (const MouseEvent&) override; | |||
/** @internal */ | |||
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override; | |||
/** @internal */ | |||
void focusGained (FocusChangeType) override; | |||
/** @internal */ | |||
void focusLost (FocusChangeType) override; | |||
/** @internal */ | |||
bool isTextInputActive() const override; | |||
/** @internal */ | |||
void setTemporaryUnderlining (const Array<Range<int> >&) override; | |||
/** @internal */ | |||
ApplicationCommandTarget* getNextCommandTarget() override; | |||
/** @internal */ | |||
void getAllCommands (Array<CommandID>&) override; | |||
/** @internal */ | |||
void getCommandInfo (CommandID, ApplicationCommandInfo&) override; | |||
/** @internal */ | |||
bool perform (const InvocationInfo&) override; | |||
private: | |||
//============================================================================== | |||
CodeDocument& document; | |||
Font font; | |||
int firstLineOnScreen = 0, spacesPerTab = 4; | |||
float charWidth = 0; | |||
int lineHeight = 0, linesOnScreen = 0, columnsOnScreen = 0; | |||
int scrollbarThickness = 16, columnToTryToMaintain = -1; | |||
bool readOnly = false, useSpacesForTabs = true, showLineNumbers = false, shouldFollowDocumentChanges = false; | |||
double xOffset = 0; | |||
CodeDocument::Position caretPos, selectionStart, selectionEnd; | |||
ScopedPointer<CaretComponent> caret; | |||
ScrollBar verticalScrollBar { true }, horizontalScrollBar { false }; | |||
ApplicationCommandManager* appCommandManager = nullptr; | |||
class Pimpl; | |||
friend class Pimpl; | |||
friend struct ContainerDeletePolicy<Pimpl>; | |||
ScopedPointer<Pimpl> pimpl; | |||
class GutterComponent; | |||
friend class GutterComponent; | |||
friend struct ContainerDeletePolicy<GutterComponent>; | |||
ScopedPointer<GutterComponent> gutter; | |||
enum DragType | |||
{ | |||
notDragging, | |||
draggingSelectionStart, | |||
draggingSelectionEnd | |||
}; | |||
DragType dragType = notDragging; | |||
//============================================================================== | |||
CodeTokeniser* codeTokeniser; | |||
ColourScheme colourScheme; | |||
class CodeEditorLine; | |||
OwnedArray<CodeEditorLine> lines; | |||
void rebuildLineTokens(); | |||
void rebuildLineTokensAsync(); | |||
void codeDocumentChanged (int start, int end); | |||
OwnedArray<CodeDocument::Iterator> cachedIterators; | |||
void clearCachedIterators (int firstLineToBeInvalid); | |||
void updateCachedIterators (int maxLineNum); | |||
void getIteratorForPosition (int position, CodeDocument::Iterator&); | |||
void moveLineDelta (int delta, bool selecting); | |||
int getGutterSize() const noexcept; | |||
//============================================================================== | |||
void insertText (const String&); | |||
virtual void updateCaretPosition(); | |||
void updateScrollBars(); | |||
void scrollToLineInternal (int line); | |||
void scrollToColumnInternal (double column); | |||
void newTransaction(); | |||
void cut(); | |||
void indentSelectedLines (int spacesToAdd); | |||
bool skipBackwardsToPreviousTab(); | |||
bool performCommand (CommandID); | |||
int indexToColumn (int line, int index) const noexcept; | |||
int columnToIndex (int line, int column) const noexcept; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CodeEditorComponent) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,58 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A base class for tokenising code so that the syntax can be displayed in a | |||
code editor. | |||
@see CodeDocument, CodeEditorComponent | |||
*/ | |||
class JUCE_API CodeTokeniser | |||
{ | |||
public: | |||
CodeTokeniser() {} | |||
virtual ~CodeTokeniser() {} | |||
//============================================================================== | |||
/** Reads the next token from the source and returns its token type. | |||
This must leave the source pointing to the first character in the | |||
next token. | |||
*/ | |||
virtual int readNextToken (CodeDocument::Iterator& source) = 0; | |||
/** Returns a suggested syntax highlighting colour scheme. */ | |||
virtual CodeEditorComponent::ColourScheme getDefaultColourScheme() = 0; | |||
private: | |||
JUCE_LEAK_DETECTOR (CodeTokeniser) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,240 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
struct LuaTokeniserFunctions | |||
{ | |||
static bool isReservedKeyword (String::CharPointerType token, const int tokenLength) noexcept | |||
{ | |||
static const char* const keywords2Char[] = | |||
{ "if", "or", "in", "do", nullptr }; | |||
static const char* const keywords3Char[] = | |||
{ "and", "end", "for", "nil", "not", nullptr }; | |||
static const char* const keywords4Char[] = | |||
{ "then", "true", "else", nullptr }; | |||
static const char* const keywords5Char[] = | |||
{ "false", "local", "until", "while", "break", nullptr }; | |||
static const char* const keywords6Char[] = | |||
{ "repeat", "return", "elseif", nullptr}; | |||
static const char* const keywordsOther[] = | |||
{ "function", "@interface", "@end", "@synthesize", "@dynamic", "@public", | |||
"@private", "@property", "@protected", "@class", nullptr }; | |||
const char* const* k; | |||
switch (tokenLength) | |||
{ | |||
case 2: k = keywords2Char; break; | |||
case 3: k = keywords3Char; break; | |||
case 4: k = keywords4Char; break; | |||
case 5: k = keywords5Char; break; | |||
case 6: k = keywords6Char; break; | |||
default: | |||
if (tokenLength < 2 || tokenLength > 16) | |||
return false; | |||
k = keywordsOther; | |||
break; | |||
} | |||
for (int i = 0; k[i] != 0; ++i) | |||
if (token.compare (CharPointer_ASCII (k[i])) == 0) | |||
return true; | |||
return false; | |||
} | |||
template <typename Iterator> | |||
static int parseIdentifier (Iterator& source) noexcept | |||
{ | |||
int tokenLength = 0; | |||
String::CharPointerType::CharType possibleIdentifier [100]; | |||
String::CharPointerType possible (possibleIdentifier); | |||
while (CppTokeniserFunctions::isIdentifierBody (source.peekNextChar())) | |||
{ | |||
auto c = source.nextChar(); | |||
if (tokenLength < 20) | |||
possible.write (c); | |||
++tokenLength; | |||
} | |||
if (tokenLength > 1 && tokenLength <= 16) | |||
{ | |||
possible.writeNull(); | |||
if (isReservedKeyword (String::CharPointerType (possibleIdentifier), tokenLength)) | |||
return LuaTokeniser::tokenType_keyword; | |||
} | |||
return LuaTokeniser::tokenType_identifier; | |||
} | |||
template <typename Iterator> | |||
static int readNextToken (Iterator& source) | |||
{ | |||
source.skipWhitespace(); | |||
auto firstChar = source.peekNextChar(); | |||
switch (firstChar) | |||
{ | |||
case 0: | |||
break; | |||
case '0': case '1': case '2': case '3': case '4': | |||
case '5': case '6': case '7': case '8': case '9': | |||
case '.': | |||
{ | |||
auto result = CppTokeniserFunctions::parseNumber (source); | |||
if (result == LuaTokeniser::tokenType_error) | |||
{ | |||
source.skip(); | |||
if (firstChar == '.') | |||
return LuaTokeniser::tokenType_punctuation; | |||
} | |||
return result; | |||
} | |||
case ',': | |||
case ';': | |||
case ':': | |||
source.skip(); | |||
return LuaTokeniser::tokenType_punctuation; | |||
case '(': case ')': | |||
case '{': case '}': | |||
case '[': case ']': | |||
source.skip(); | |||
return LuaTokeniser::tokenType_bracket; | |||
case '"': | |||
case '\'': | |||
CppTokeniserFunctions::skipQuotedString (source); | |||
return LuaTokeniser::tokenType_string; | |||
case '+': | |||
source.skip(); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '+', '='); | |||
return LuaTokeniser::tokenType_operator; | |||
case '-': | |||
{ | |||
source.skip(); | |||
auto result = CppTokeniserFunctions::parseNumber (source); | |||
if (source.peekNextChar() == '-') | |||
{ | |||
source.skipToEndOfLine(); | |||
return LuaTokeniser::tokenType_comment; | |||
} | |||
if (result == LuaTokeniser::tokenType_error) | |||
{ | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '-', '='); | |||
return LuaTokeniser::tokenType_operator; | |||
} | |||
return result; | |||
} | |||
case '*': case '%': | |||
case '=': case '!': | |||
source.skip(); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '='); | |||
return LuaTokeniser::tokenType_operator; | |||
case '?': | |||
case '~': | |||
source.skip(); | |||
return LuaTokeniser::tokenType_operator; | |||
case '<': case '>': | |||
case '|': case '&': case '^': | |||
source.skip(); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, firstChar); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '='); | |||
return LuaTokeniser::tokenType_operator; | |||
default: | |||
if (CppTokeniserFunctions::isIdentifierStart (firstChar)) | |||
return parseIdentifier (source); | |||
source.skip(); | |||
break; | |||
} | |||
return LuaTokeniser::tokenType_error; | |||
} | |||
}; | |||
//============================================================================== | |||
LuaTokeniser::LuaTokeniser() {} | |||
LuaTokeniser::~LuaTokeniser() {} | |||
int LuaTokeniser::readNextToken (CodeDocument::Iterator& source) | |||
{ | |||
return LuaTokeniserFunctions::readNextToken (source); | |||
} | |||
CodeEditorComponent::ColourScheme LuaTokeniser::getDefaultColourScheme() | |||
{ | |||
static const CodeEditorComponent::ColourScheme::TokenType types[] = | |||
{ | |||
{ "Error", Colour (0xffcc0000) }, | |||
{ "Comment", Colour (0xff3c3c3c) }, | |||
{ "Keyword", Colour (0xff0000cc) }, | |||
{ "Operator", Colour (0xff225500) }, | |||
{ "Identifier", Colour (0xff000000) }, | |||
{ "Integer", Colour (0xff880000) }, | |||
{ "Float", Colour (0xff885500) }, | |||
{ "String", Colour (0xff990099) }, | |||
{ "Bracket", Colour (0xff000055) }, | |||
{ "Punctuation", Colour (0xff004400) } | |||
}; | |||
CodeEditorComponent::ColourScheme cs; | |||
for (auto& t : types) | |||
cs.set (t.name, Colour (t.colour)); | |||
return cs; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,64 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
*/ | |||
class JUCE_API LuaTokeniser : public CodeTokeniser | |||
{ | |||
public: | |||
//============================================================================== | |||
LuaTokeniser(); | |||
~LuaTokeniser(); | |||
//============================================================================== | |||
int readNextToken (CodeDocument::Iterator&) override; | |||
CodeEditorComponent::ColourScheme getDefaultColourScheme() override; | |||
/** The token values returned by this tokeniser. */ | |||
enum TokenType | |||
{ | |||
tokenType_error = 0, | |||
tokenType_comment, | |||
tokenType_keyword, | |||
tokenType_operator, | |||
tokenType_identifier, | |||
tokenType_integer, | |||
tokenType_float, | |||
tokenType_string, | |||
tokenType_bracket, | |||
tokenType_punctuation | |||
}; | |||
private: | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LuaTokeniser) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,173 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
XmlTokeniser::XmlTokeniser() {} | |||
XmlTokeniser::~XmlTokeniser() {} | |||
CodeEditorComponent::ColourScheme XmlTokeniser::getDefaultColourScheme() | |||
{ | |||
struct Type | |||
{ | |||
const char* name; | |||
uint32 colour; | |||
}; | |||
const Type types[] = | |||
{ | |||
{ "Error", 0xffcc0000 }, | |||
{ "Comment", 0xff00aa00 }, | |||
{ "Keyword", 0xff0000cc }, | |||
{ "Operator", 0xff225500 }, | |||
{ "Identifier", 0xff000000 }, | |||
{ "String", 0xff990099 }, | |||
{ "Bracket", 0xff000055 }, | |||
{ "Punctuation", 0xff004400 }, | |||
{ "Preprocessor Text", 0xff660000 } | |||
}; | |||
CodeEditorComponent::ColourScheme cs; | |||
for (auto& t : types) | |||
cs.set (t.name, Colour (t.colour)); | |||
return cs; | |||
} | |||
template <typename Iterator> | |||
static void skipToEndOfXmlDTD (Iterator& source) noexcept | |||
{ | |||
bool lastWasQuestionMark = false; | |||
for (;;) | |||
{ | |||
auto c = source.nextChar(); | |||
if (c == 0 || (c == '>' && lastWasQuestionMark)) | |||
break; | |||
lastWasQuestionMark = (c == '?'); | |||
} | |||
} | |||
template <typename Iterator> | |||
static void skipToEndOfXmlComment (Iterator& source) noexcept | |||
{ | |||
juce_wchar last[2] = {}; | |||
for (;;) | |||
{ | |||
auto c = source.nextChar(); | |||
if (c == 0 || (c == '>' && last[0] == '-' && last[1] == '-')) | |||
break; | |||
last[1] = last[0]; | |||
last[0] = c; | |||
} | |||
} | |||
int XmlTokeniser::readNextToken (CodeDocument::Iterator& source) | |||
{ | |||
source.skipWhitespace(); | |||
auto firstChar = source.peekNextChar(); | |||
switch (firstChar) | |||
{ | |||
case 0: break; | |||
case '"': | |||
case '\'': | |||
CppTokeniserFunctions::skipQuotedString (source); | |||
return tokenType_string; | |||
case '<': | |||
{ | |||
source.skip(); | |||
source.skipWhitespace(); | |||
auto nextChar = source.peekNextChar(); | |||
if (nextChar == '?') | |||
{ | |||
source.skip(); | |||
skipToEndOfXmlDTD (source); | |||
return tokenType_preprocessor; | |||
} | |||
if (nextChar == '!') | |||
{ | |||
source.skip(); | |||
if (source.peekNextChar() == '-') | |||
{ | |||
source.skip(); | |||
if (source.peekNextChar() == '-') | |||
{ | |||
skipToEndOfXmlComment (source); | |||
return tokenType_comment; | |||
} | |||
} | |||
} | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '/'); | |||
CppTokeniserFunctions::parseIdentifier (source); | |||
source.skipWhitespace(); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '/'); | |||
source.skipWhitespace(); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '>'); | |||
return tokenType_keyword; | |||
} | |||
case '>': | |||
source.skip(); | |||
return tokenType_keyword; | |||
case '/': | |||
source.skip(); | |||
source.skipWhitespace(); | |||
CppTokeniserFunctions::skipIfNextCharMatches (source, '>'); | |||
return tokenType_keyword; | |||
case '=': | |||
case ':': | |||
source.skip(); | |||
return tokenType_operator; | |||
default: | |||
if (CppTokeniserFunctions::isIdentifierStart (firstChar)) | |||
CppTokeniserFunctions::parseIdentifier (source); | |||
source.skip(); | |||
break; | |||
}; | |||
return tokenType_identifier; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,62 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
*/ | |||
class JUCE_API XmlTokeniser : public CodeTokeniser | |||
{ | |||
public: | |||
//============================================================================== | |||
XmlTokeniser(); | |||
~XmlTokeniser(); | |||
//============================================================================== | |||
int readNextToken (CodeDocument::Iterator&) override; | |||
CodeEditorComponent::ColourScheme getDefaultColourScheme() override; | |||
/** The token values returned by this tokeniser. */ | |||
enum TokenType | |||
{ | |||
tokenType_error = 0, | |||
tokenType_comment, | |||
tokenType_keyword, | |||
tokenType_operator, | |||
tokenType_identifier, | |||
tokenType_string, | |||
tokenType_bracket, | |||
tokenType_punctuation, | |||
tokenType_preprocessor | |||
}; | |||
private: | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XmlTokeniser) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,266 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
FileBasedDocument::FileBasedDocument (const String& fileExtension_, | |||
const String& fileWildcard_, | |||
const String& openFileDialogTitle_, | |||
const String& saveFileDialogTitle_) | |||
: changedSinceSave (false), | |||
fileExtension (fileExtension_), | |||
fileWildcard (fileWildcard_), | |||
openFileDialogTitle (openFileDialogTitle_), | |||
saveFileDialogTitle (saveFileDialogTitle_) | |||
{ | |||
} | |||
FileBasedDocument::~FileBasedDocument() | |||
{ | |||
} | |||
//============================================================================== | |||
void FileBasedDocument::setChangedFlag (const bool hasChanged) | |||
{ | |||
if (changedSinceSave != hasChanged) | |||
{ | |||
changedSinceSave = hasChanged; | |||
sendChangeMessage(); | |||
} | |||
} | |||
void FileBasedDocument::changed() | |||
{ | |||
changedSinceSave = true; | |||
sendChangeMessage(); | |||
} | |||
//============================================================================== | |||
void FileBasedDocument::setFile (const File& newFile) | |||
{ | |||
if (documentFile != newFile) | |||
{ | |||
documentFile = newFile; | |||
changed(); | |||
} | |||
} | |||
//============================================================================== | |||
Result FileBasedDocument::loadFrom (const File& newFile, const bool showMessageOnFailure) | |||
{ | |||
MouseCursor::showWaitCursor(); | |||
const File oldFile (documentFile); | |||
documentFile = newFile; | |||
Result result (Result::fail (TRANS("The file doesn't exist"))); | |||
if (newFile.existsAsFile()) | |||
{ | |||
result = loadDocument (newFile); | |||
if (result.wasOk()) | |||
{ | |||
setChangedFlag (false); | |||
MouseCursor::hideWaitCursor(); | |||
setLastDocumentOpened (newFile); | |||
return result; | |||
} | |||
} | |||
documentFile = oldFile; | |||
MouseCursor::hideWaitCursor(); | |||
if (showMessageOnFailure) | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Failed to open file..."), | |||
TRANS("There was an error while trying to load the file: FLNM") | |||
.replace ("FLNM", "\n" + newFile.getFullPathName()) | |||
+ "\n\n" | |||
+ result.getErrorMessage()); | |||
return result; | |||
} | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
Result FileBasedDocument::loadFromUserSpecifiedFile (const bool showMessageOnFailure) | |||
{ | |||
FileChooser fc (openFileDialogTitle, | |||
getLastDocumentOpened(), | |||
fileWildcard); | |||
if (fc.browseForFileToOpen()) | |||
return loadFrom (fc.getResult(), showMessageOnFailure); | |||
return Result::fail (TRANS("User cancelled")); | |||
} | |||
static bool askToOverwriteFile (const File& newFile) | |||
{ | |||
return AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, | |||
TRANS("File already exists"), | |||
TRANS("There's already a file called: FLNM") | |||
.replace ("FLNM", newFile.getFullPathName()) | |||
+ "\n\n" | |||
+ TRANS("Are you sure you want to overwrite it?"), | |||
TRANS("Overwrite"), | |||
TRANS("Cancel")); | |||
} | |||
//============================================================================== | |||
FileBasedDocument::SaveResult FileBasedDocument::save (const bool askUserForFileIfNotSpecified, | |||
const bool showMessageOnFailure) | |||
{ | |||
return saveAs (documentFile, | |||
false, | |||
askUserForFileIfNotSpecified, | |||
showMessageOnFailure); | |||
} | |||
FileBasedDocument::SaveResult FileBasedDocument::saveAs (const File& newFile, | |||
const bool warnAboutOverwritingExistingFiles, | |||
const bool askUserForFileIfNotSpecified, | |||
const bool showMessageOnFailure) | |||
{ | |||
if (newFile == File()) | |||
{ | |||
if (askUserForFileIfNotSpecified) | |||
return saveAsInteractive (true); | |||
// can't save to an unspecified file | |||
jassertfalse; | |||
return failedToWriteToFile; | |||
} | |||
if (warnAboutOverwritingExistingFiles | |||
&& newFile.exists() | |||
&& ! askToOverwriteFile (newFile)) | |||
return userCancelledSave; | |||
MouseCursor::showWaitCursor(); | |||
const File oldFile (documentFile); | |||
documentFile = newFile; | |||
const Result result (saveDocument (newFile)); | |||
if (result.wasOk()) | |||
{ | |||
setChangedFlag (false); | |||
MouseCursor::hideWaitCursor(); | |||
sendChangeMessage(); // because the filename may have changed | |||
return savedOk; | |||
} | |||
documentFile = oldFile; | |||
MouseCursor::hideWaitCursor(); | |||
if (showMessageOnFailure) | |||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, | |||
TRANS("Error writing to file..."), | |||
TRANS("An error occurred while trying to save \"DCNM\" to the file: FLNM") | |||
.replace ("DCNM", getDocumentTitle()) | |||
.replace ("FLNM", "\n" + newFile.getFullPathName()) | |||
+ "\n\n" | |||
+ result.getErrorMessage()); | |||
sendChangeMessage(); // because the filename may have changed | |||
return failedToWriteToFile; | |||
} | |||
FileBasedDocument::SaveResult FileBasedDocument::saveIfNeededAndUserAgrees() | |||
{ | |||
if (! hasChangedSinceSaved()) | |||
return savedOk; | |||
const int r = AlertWindow::showYesNoCancelBox (AlertWindow::QuestionIcon, | |||
TRANS("Closing document..."), | |||
TRANS("Do you want to save the changes to \"DCNM\"?") | |||
.replace ("DCNM", getDocumentTitle()), | |||
TRANS("Save"), | |||
TRANS("Discard changes"), | |||
TRANS("Cancel")); | |||
if (r == 1) // save changes | |||
return save (true, true); | |||
if (r == 2) // discard changes | |||
return savedOk; | |||
return userCancelledSave; | |||
} | |||
File FileBasedDocument::getSuggestedSaveAsFile (const File& defaultFile) | |||
{ | |||
return defaultFile.withFileExtension (fileExtension).getNonexistentSibling (true); | |||
} | |||
FileBasedDocument::SaveResult FileBasedDocument::saveAsInteractive (const bool warnAboutOverwritingExistingFiles) | |||
{ | |||
File f; | |||
if (documentFile.existsAsFile()) | |||
f = documentFile; | |||
else | |||
f = getLastDocumentOpened(); | |||
String legalFilename (File::createLegalFileName (getDocumentTitle())); | |||
if (legalFilename.isEmpty()) | |||
legalFilename = "unnamed"; | |||
if (f.existsAsFile() || f.getParentDirectory().isDirectory()) | |||
f = f.getSiblingFile (legalFilename); | |||
else | |||
f = File::getSpecialLocation (File::userDocumentsDirectory).getChildFile (legalFilename); | |||
f = getSuggestedSaveAsFile (f); | |||
FileChooser fc (saveFileDialogTitle, f, fileWildcard); | |||
if (fc.browseForFileToSave (warnAboutOverwritingExistingFiles)) | |||
{ | |||
File chosen (fc.getResult()); | |||
if (chosen.getFileExtension().isEmpty()) | |||
{ | |||
chosen = chosen.withFileExtension (fileExtension); | |||
if (chosen.exists() && ! askToOverwriteFile (chosen)) | |||
return userCancelledSave; | |||
} | |||
setLastDocumentOpened (chosen); | |||
return saveAs (chosen, false, false, true); | |||
} | |||
return userCancelledSave; | |||
} | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,294 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A class to take care of the logic involved with the loading/saving of some kind | |||
of document. | |||
There's quite a lot of tedious logic involved in writing all the load/save/save-as | |||
functions you need for documents that get saved to a file, so this class attempts | |||
to abstract most of the boring stuff. | |||
Your subclass should just implement all the pure virtual methods, and you can | |||
then use the higher-level public methods to do the load/save dialogs, to warn the user | |||
about overwriting files, etc. | |||
The document object keeps track of whether it has changed since it was last saved or | |||
loaded, so when you change something, call its changed() method. This will set a | |||
flag so it knows it needs saving, and will also broadcast a change message using the | |||
ChangeBroadcaster base class. | |||
@see ChangeBroadcaster | |||
*/ | |||
class JUCE_API FileBasedDocument : public ChangeBroadcaster | |||
{ | |||
public: | |||
/** Creates a FileBasedDocument. | |||
@param fileExtension the extension to use when loading/saving files, e.g. ".doc" | |||
@param fileWildCard the wildcard to use in file dialogs, e.g. "*.doc" | |||
@param openFileDialogTitle the title to show on an open-file dialog, e.g. "Choose a file to open.." | |||
@param saveFileDialogTitle the title to show on an save-file dialog, e.g. "Choose a file to save as.." | |||
*/ | |||
FileBasedDocument (const String& fileExtension, | |||
const String& fileWildCard, | |||
const String& openFileDialogTitle, | |||
const String& saveFileDialogTitle); | |||
/** Destructor. */ | |||
virtual ~FileBasedDocument(); | |||
//============================================================================== | |||
/** Returns true if the changed() method has been called since the file was | |||
last saved or loaded. | |||
@see setChangedFlag, changed | |||
*/ | |||
bool hasChangedSinceSaved() const { return changedSinceSave; } | |||
/** Called to indicate that the document has changed and needs saving. | |||
This method will also trigger a change message to be sent out using the | |||
ChangeBroadcaster base class. | |||
After calling the method, the hasChangedSinceSaved() method will return true, until | |||
it is reset either by saving to a file or using the setChangedFlag() method. | |||
@see hasChangedSinceSaved, setChangedFlag | |||
*/ | |||
virtual void changed(); | |||
/** Sets the state of the 'changed' flag. | |||
The 'changed' flag is set to true when the changed() method is called - use this method | |||
to reset it or to set it without also broadcasting a change message. | |||
@see changed, hasChangedSinceSaved | |||
*/ | |||
void setChangedFlag (bool hasChanged); | |||
//============================================================================== | |||
/** Tries to open a file. | |||
If the file opens correctly, the document's file (see the getFile() method) is set | |||
to this new one; if it fails, the document's file is left unchanged, and optionally | |||
a message box is shown telling the user there was an error. | |||
@returns A result indicating whether the new file loaded successfully, or the error | |||
message if it failed. | |||
@see loadDocument, loadFromUserSpecifiedFile | |||
*/ | |||
Result loadFrom (const File& fileToLoadFrom, | |||
bool showMessageOnFailure); | |||
/** Asks the user for a file and tries to load it. | |||
This will pop up a dialog box using the title, file extension and | |||
wildcard specified in the document's constructor, and asks the user | |||
for a file. If they pick one, the loadFrom() method is used to | |||
try to load it, optionally showing a message if it fails. | |||
@returns a result indicating success; This will be a failure message if the user | |||
cancelled or if they picked a file which failed to load correctly | |||
@see loadFrom | |||
*/ | |||
Result loadFromUserSpecifiedFile (bool showMessageOnFailure); | |||
//============================================================================== | |||
/** A set of possible outcomes of one of the save() methods | |||
*/ | |||
enum SaveResult | |||
{ | |||
savedOk = 0, /**< indicates that a file was saved successfully. */ | |||
userCancelledSave, /**< indicates that the user aborted the save operation. */ | |||
failedToWriteToFile /**< indicates that it tried to write to a file but this failed. */ | |||
}; | |||
/** Tries to save the document to the last file it was saved or loaded from. | |||
This will always try to write to the file, even if the document isn't flagged as | |||
having changed. | |||
@param askUserForFileIfNotSpecified if there's no file currently specified and this is | |||
true, it will prompt the user to pick a file, as if | |||
saveAsInteractive() was called. | |||
@param showMessageOnFailure if true it will show a warning message when if the | |||
save operation fails | |||
@see saveIfNeededAndUserAgrees, saveAs, saveAsInteractive | |||
*/ | |||
SaveResult save (bool askUserForFileIfNotSpecified, | |||
bool showMessageOnFailure); | |||
/** If the file needs saving, it'll ask the user if that's what they want to do, and save | |||
it if they say yes. | |||
If you've got a document open and want to close it (e.g. to quit the app), this is the | |||
method to call. | |||
If the document doesn't need saving it'll return the value savedOk so | |||
you can go ahead and delete the document. | |||
If it does need saving it'll prompt the user, and if they say "discard changes" it'll | |||
return savedOk, so again, you can safely delete the document. | |||
If the user clicks "cancel", it'll return userCancelledSave, so if you can abort the | |||
close-document operation. | |||
And if they click "save changes", it'll try to save and either return savedOk, or | |||
failedToWriteToFile if there was a problem. | |||
@see save, saveAs, saveAsInteractive | |||
*/ | |||
SaveResult saveIfNeededAndUserAgrees(); | |||
/** Tries to save the document to a specified file. | |||
If this succeeds, it'll also change the document's internal file (as returned by | |||
the getFile() method). If it fails, the file will be left unchanged. | |||
@param newFile the file to try to write to | |||
@param warnAboutOverwritingExistingFiles if true and the file exists, it'll ask | |||
the user first if they want to overwrite it | |||
@param askUserForFileIfNotSpecified if the file is non-existent and this is true, it'll | |||
use the saveAsInteractive() method to ask the user for a | |||
filename | |||
@param showMessageOnFailure if true and the write operation fails, it'll show | |||
a message box to warn the user | |||
@see saveIfNeededAndUserAgrees, save, saveAsInteractive | |||
*/ | |||
SaveResult saveAs (const File& newFile, | |||
bool warnAboutOverwritingExistingFiles, | |||
bool askUserForFileIfNotSpecified, | |||
bool showMessageOnFailure); | |||
/** Prompts the user for a filename and tries to save to it. | |||
This will pop up a dialog box using the title, file extension and | |||
wildcard specified in the document's constructor, and asks the user | |||
for a file. If they pick one, the saveAs() method is used to try to save | |||
to this file. | |||
@param warnAboutOverwritingExistingFiles if true and the file exists, it'll ask | |||
the user first if they want to overwrite it | |||
@see saveIfNeededAndUserAgrees, save, saveAs | |||
*/ | |||
SaveResult saveAsInteractive (bool warnAboutOverwritingExistingFiles); | |||
//============================================================================== | |||
/** Returns the file that this document was last successfully saved or loaded from. | |||
When the document object is created, this will be set to File(). | |||
It is changed when one of the load or save methods is used, or when setFile() | |||
is used to explicitly set it. | |||
*/ | |||
const File& getFile() const { return documentFile; } | |||
/** Sets the file that this document thinks it was loaded from. | |||
This won't actually load anything - it just changes the file stored internally. | |||
@see getFile | |||
*/ | |||
void setFile (const File& newFile); | |||
protected: | |||
//============================================================================== | |||
/** Overload this to return the title of the document. | |||
This is used in message boxes, filenames and file choosers, so it should be | |||
something sensible. | |||
*/ | |||
virtual String getDocumentTitle() = 0; | |||
/** This method should try to load your document from the given file. | |||
@returns a Result object to indicate the whether there was an error. | |||
*/ | |||
virtual Result loadDocument (const File& file) = 0; | |||
/** This method should try to write your document to the given file. | |||
@returns a Result object to indicate the whether there was an error. | |||
*/ | |||
virtual Result saveDocument (const File& file) = 0; | |||
/** This is used for dialog boxes to make them open at the last folder you | |||
were using. | |||
getLastDocumentOpened() and setLastDocumentOpened() are used to store | |||
the last document that was used - you might want to store this value | |||
in a static variable, or even in your application's properties. It should | |||
be a global setting rather than a property of this object. | |||
This method works very well in conjunction with a RecentlyOpenedFilesList | |||
object to manage your recent-files list. | |||
As a default value, it's ok to return File(), and the document object will | |||
use a sensible one instead. | |||
@see RecentlyOpenedFilesList | |||
*/ | |||
virtual File getLastDocumentOpened() = 0; | |||
/** This is used for dialog boxes to make them open at the last folder you | |||
were using. | |||
getLastDocumentOpened() and setLastDocumentOpened() are used to store | |||
the last document that was used - you might want to store this value | |||
in a static variable, or even in your application's properties. It should | |||
be a global setting rather than a property of this object. | |||
This method works very well in conjunction with a RecentlyOpenedFilesList | |||
object to manage your recent-files list. | |||
@see RecentlyOpenedFilesList | |||
*/ | |||
virtual void setLastDocumentOpened (const File& file) = 0; | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
/** This is called by saveAsInteractive() to allow you to optionally customise the | |||
filename that the user is presented with in the save dialog. | |||
The defaultFile parameter is an initial suggestion based on what the class knows | |||
about the current document - you can return a variation on this file with a different | |||
extension, etc, or just return something completely different. | |||
*/ | |||
virtual File getSuggestedSaveAsFile (const File& defaultFile); | |||
#endif | |||
private: | |||
//============================================================================== | |||
File documentFile; | |||
bool changedSinceSave; | |||
String fileExtension, fileWildcard, openFileDialogTitle, saveFileDialogTitle; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileBasedDocument) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,128 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_WINDOWS || DOXYGEN | |||
//============================================================================== | |||
/** | |||
A Windows-specific class that can create and embed an ActiveX control inside | |||
itself. | |||
To use it, create one of these, put it in place and make sure it's visible in a | |||
window, then use createControl() to instantiate an ActiveX control. The control | |||
will then be moved and resized to follow the movements of this component. | |||
Of course, since the control is a heavyweight window, it'll obliterate any | |||
juce components that may overlap this component, but that's life. | |||
*/ | |||
class JUCE_API ActiveXControlComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Create an initially-empty container. */ | |||
ActiveXControlComponent(); | |||
/** Destructor. */ | |||
~ActiveXControlComponent(); | |||
/** Tries to create an ActiveX control and embed it in this peer. | |||
The peer controlIID is a pointer to an IID structure - it's treated | |||
as a void* because when including the Juce headers, you might not always | |||
have included windows.h first, in which case IID wouldn't be defined. | |||
e.g. @code | |||
const IID myIID = __uuidof (QTControl); | |||
myControlComp->createControl (&myIID); | |||
@endcode | |||
*/ | |||
bool createControl (const void* controlIID); | |||
/** Deletes the ActiveX control, if one has been created. | |||
*/ | |||
void deleteControl(); | |||
/** Returns true if a control is currently in use. */ | |||
bool isControlOpen() const noexcept { return control != nullptr; } | |||
/** Does a QueryInterface call on the embedded control object. | |||
This allows you to cast the control to whatever type of COM object you need. | |||
The iid parameter is a pointer to an IID structure - it's treated | |||
as a void* because when including the Juce headers, you might not always | |||
have included windows.h first, in which case IID wouldn't be defined, but | |||
you should just pass a pointer to an IID. | |||
e.g. @code | |||
const IID iid = __uuidof (IOleWindow); | |||
IOleWindow* oleWindow = (IOleWindow*) myControlComp->queryInterface (&iid); | |||
if (oleWindow != nullptr) | |||
{ | |||
HWND hwnd; | |||
oleWindow->GetWindow (&hwnd); | |||
... | |||
oleWindow->Release(); | |||
} | |||
@endcode | |||
*/ | |||
void* queryInterface (const void* iid) const; | |||
/** Set this to false to stop mouse events being allowed through to the control. | |||
*/ | |||
void setMouseEventsAllowed (bool eventsCanReachControl); | |||
/** Returns true if mouse events are allowed to get through to the control. | |||
*/ | |||
bool areMouseEventsAllowed() const noexcept { return mouseEventsAllowed; } | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
intptr_t offerEventToActiveXControl (void*); | |||
static intptr_t offerEventToActiveXControlStatic (void*); | |||
private: | |||
class Pimpl; | |||
friend struct ContainerDeletePolicy<Pimpl>; | |||
ScopedPointer<Pimpl> control; | |||
bool mouseEventsAllowed = true; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ActiveXControlComponent) | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,90 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_MAC || DOXYGEN | |||
//============================================================================== | |||
/** | |||
A Mac-specific class that can create and embed an NSView inside itself. | |||
To use it, create one of these, put it in place and make sure it's visible in a | |||
window, then use setView() to assign an NSView to it. The view will then be | |||
moved and resized to follow the movements of this component. | |||
Of course, since the view is a native object, it'll obliterate any | |||
juce components that may overlap this component, but that's life. | |||
*/ | |||
class JUCE_API NSViewComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Create an initially-empty container. */ | |||
NSViewComponent(); | |||
/** Destructor. */ | |||
~NSViewComponent(); | |||
/** Assigns an NSView to this peer. | |||
The view will be retained and released by this component for as long as | |||
it is needed. To remove the current view, just call setView (nullptr). | |||
Note: a void* is used here to avoid including the cocoa headers as | |||
part of the juce.h, but the method expects an NSView*. | |||
*/ | |||
void setView (void* nsView); | |||
/** Returns the current NSView. | |||
Note: a void* is returned here to avoid the needing to include the cocoa | |||
headers, so you should just cast the return value to an NSView*. | |||
*/ | |||
void* getView() const; | |||
/** Resizes this component to fit the view that it contains. */ | |||
void resizeToFitView(); | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
void alphaChanged() override; | |||
/** @internal */ | |||
static ReferenceCountedObject* attachViewToComponent (Component&, void*); | |||
private: | |||
ReferenceCountedObjectPtr<ReferenceCountedObject> attachment; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NSViewComponent) | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,89 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_IOS || DOXYGEN | |||
//============================================================================== | |||
/** | |||
An iOS-specific class that can create and embed an UIView inside itself. | |||
To use it, create one of these, put it in place and make sure it's visible in a | |||
window, then use setView() to assign a UIView to it. The view will then be | |||
moved and resized to follow the movements of this component. | |||
Of course, since the view is a native object, it'll obliterate any | |||
juce components that may overlap this component, but that's life. | |||
*/ | |||
class JUCE_API UIViewComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Create an initially-empty container. */ | |||
UIViewComponent(); | |||
/** Destructor. */ | |||
~UIViewComponent(); | |||
/** Assigns an UIView to this peer. | |||
The view will be retained and released by this component for as long as | |||
it is needed. To remove the current view, just call setView (nullptr). | |||
Note: a void* is used here to avoid including the cocoa headers as | |||
part of the juce.h, but the method expects an UIView*. | |||
*/ | |||
void setView (void* uiView); | |||
/** Returns the current UIView. | |||
Note: a void* is returned here to avoid the needing to include the cocoa | |||
headers, so you should just cast the return value to an UIView*. | |||
*/ | |||
void* getView() const; | |||
/** Resizes this component to fit the view that it contains. */ | |||
void resizeToFitView(); | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
private: | |||
class Pimpl; | |||
friend class Pimpl; | |||
ScopedPointer<Pimpl> pimpl; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIViewComponent) | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,114 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** @internal */ | |||
bool juce_handleXEmbedEvent (ComponentPeer*, void*); | |||
/** @internal */ | |||
unsigned long juce_getCurrentFocusWindow (ComponentPeer*); | |||
#if JUCE_LINUX || DOXYGEN | |||
//============================================================================== | |||
/** | |||
A Linux-specific class that can embed a foreign X11 widget. | |||
Use this class to embed a foreign X11 widget from other toolkits such as | |||
GTK+ or QT. | |||
There are two ways to initiate the Xembed protocol. Either the client creates | |||
a window and passes this to the host (client initiated) or the host | |||
creates a window in which the client can reparent it's client widget | |||
(host initiated). XEmbedComponent supports both protocol types. | |||
This is how you embed a GTK+ widget: if you are using the client | |||
initiated version of the protocol, then create a new gtk widget with | |||
gtk_plug_new (0). Then query the window id of the plug via gtk_plug_get_id(). | |||
Pass this id to the constructor of this class. | |||
If you are using the host initiated version of the protocol, then first create | |||
the XEmbedComponent using the default constructor. Use getHostWindowID to get | |||
the window id of the host, use this to construct your gtk plug via gtk_plug_new. | |||
A similar approach can be used to embed QT widgets via QT's QX11EmbedWidget | |||
class. | |||
Other toolkits or raw X11 widgets should follow the X11 embed protocol: | |||
https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html | |||
*/ | |||
class XEmbedComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates a JUCE component wrapping a foreign widget | |||
Use this constructor if you are using the host initiated version | |||
of the XEmbedProtocol. When using this version of the protocol | |||
you must call getHostWindowID() and pass this id to the foreign toolkit. | |||
*/ | |||
XEmbedComponent (bool wantsKeyboardFocus = true, | |||
bool allowForeignWidgetToResizeComponent = false); | |||
/** Create a JUCE component wrapping the foreign widget with id wID | |||
Use this constructor if you are using the client initiated version | |||
of the XEmbedProtocol. | |||
*/ | |||
XEmbedComponent (unsigned long wID, bool wantsKeyboardFocus = true, | |||
bool allowForeignWidgetToResizeComponent = false); | |||
/** Destructor. */ | |||
~XEmbedComponent(); | |||
/** Use this method to retrieve the host's window id when using the | |||
host initiated version of the XEmbedProtocol | |||
*/ | |||
unsigned long getHostWindowID(); | |||
protected: | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
void focusGained (FocusChangeType) override; | |||
void focusLost (FocusChangeType) override; | |||
void broughtToFront() override; | |||
private: | |||
friend bool juce::juce_handleXEmbedEvent (ComponentPeer*, void*); | |||
friend unsigned long juce_getCurrentFocusWindow (ComponentPeer*); | |||
class Pimpl; | |||
friend struct ContainerDeletePolicy<Pimpl>; | |||
ScopedPointer<Pimpl> pimpl; | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,160 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
#ifdef JUCE_GUI_EXTRA_H_INCLUDED | |||
/* When you add this cpp file to your project, you mustn't include it in a file where you've | |||
already included any other headers - just put it inside a file on its own, possibly with your config | |||
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix | |||
header files that the compiler may be using. | |||
*/ | |||
#error "Incorrect use of JUCE cpp file" | |||
#endif | |||
#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1 | |||
#define JUCE_CORE_INCLUDE_COM_SMART_PTR 1 | |||
#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1 | |||
#define JUCE_EVENTS_INCLUDE_WIN32_MESSAGE_WINDOW 1 | |||
#define JUCE_GRAPHICS_INCLUDE_COREGRAPHICS_HELPERS 1 | |||
#include "juce_gui_extra.h" | |||
//============================================================================== | |||
#if JUCE_MAC | |||
#import <WebKit/WebKit.h> | |||
#import <IOKit/IOKitLib.h> | |||
#import <IOKit/IOCFPlugIn.h> | |||
#import <IOKit/hid/IOHIDLib.h> | |||
#import <IOKit/hid/IOHIDKeys.h> | |||
#import <IOKit/pwr_mgt/IOPMLib.h> | |||
#elif JUCE_IOS | |||
//============================================================================== | |||
#elif JUCE_WINDOWS | |||
#include <windowsx.h> | |||
#include <vfw.h> | |||
#include <commdlg.h> | |||
#if JUCE_WEB_BROWSER | |||
#include <exdisp.h> | |||
#include <exdispid.h> | |||
#endif | |||
//============================================================================== | |||
#elif JUCE_LINUX | |||
#include <X11/Xlib.h> | |||
#include <X11/Xatom.h> | |||
#include <X11/Xutil.h> | |||
#undef SIZEOF | |||
#undef KeyPress | |||
#if JUCE_WEB_BROWSER | |||
#include <unistd.h> | |||
#include <fcntl.h> | |||
#include <sys/wait.h> | |||
#include <gtk/gtk.h> | |||
#include <gtk/gtkx.h> | |||
#include <glib-unix.h> | |||
#include <webkit2/webkit2.h> | |||
#endif | |||
#endif | |||
//============================================================================== | |||
#include "documents/juce_FileBasedDocument.cpp" | |||
#include "code_editor/juce_CodeDocument.cpp" | |||
#include "code_editor/juce_CodeEditorComponent.cpp" | |||
#include "code_editor/juce_CPlusPlusCodeTokeniser.cpp" | |||
#include "code_editor/juce_XMLCodeTokeniser.cpp" | |||
#include "code_editor/juce_LuaCodeTokeniser.cpp" | |||
#include "misc/juce_BubbleMessageComponent.cpp" | |||
#include "misc/juce_ColourSelector.cpp" | |||
#include "misc/juce_KeyMappingEditorComponent.cpp" | |||
#include "misc/juce_PreferencesPanel.cpp" | |||
#include "misc/juce_RecentlyOpenedFilesList.cpp" | |||
#include "misc/juce_SplashScreen.cpp" | |||
#include "misc/juce_SystemTrayIconComponent.cpp" | |||
#include "misc/juce_LiveConstantEditor.cpp" | |||
#include "misc/juce_AnimatedAppComponent.cpp" | |||
//============================================================================== | |||
#if JUCE_MAC || JUCE_IOS | |||
#if JUCE_CLANG | |||
#pragma clang diagnostic push | |||
#pragma clang diagnostic ignored "-Wundeclared-selector" | |||
#endif | |||
#if JUCE_MAC | |||
#include "native/juce_mac_NSViewComponent.mm" | |||
#include "native/juce_mac_AppleRemote.mm" | |||
#include "native/juce_mac_SystemTrayIcon.cpp" | |||
#endif | |||
#if JUCE_IOS | |||
#include "native/juce_ios_UIViewComponent.mm" | |||
#endif | |||
#if JUCE_WEB_BROWSER | |||
#include "native/juce_mac_WebBrowserComponent.mm" | |||
#endif | |||
#if JUCE_CLANG | |||
#pragma clang diagnostic pop | |||
#endif | |||
//============================================================================== | |||
#elif JUCE_WINDOWS | |||
#include "native/juce_win32_ActiveXComponent.cpp" | |||
#if JUCE_WEB_BROWSER | |||
#include "native/juce_win32_WebBrowserComponent.cpp" | |||
#endif | |||
#include "native/juce_win32_SystemTrayIcon.cpp" | |||
//============================================================================== | |||
#elif JUCE_LINUX | |||
#include "native/juce_linux_XEmbedComponent.cpp" | |||
#if JUCE_WEB_BROWSER | |||
#include "native/juce_linux_X11_WebBrowserComponent.cpp" | |||
#endif | |||
#include "native/juce_linux_X11_SystemTrayIcon.cpp" | |||
//============================================================================== | |||
#elif JUCE_ANDROID | |||
#if JUCE_WEB_BROWSER | |||
#include "native/juce_android_WebBrowserComponent.cpp" | |||
#endif | |||
#endif | |||
#if JUCE_WEB_BROWSER | |||
namespace juce | |||
{ | |||
bool WebBrowserComponent::pageAboutToLoad (const String&) { return true; } | |||
void WebBrowserComponent::pageFinishedLoading (const String&) {} | |||
bool WebBrowserComponent::pageLoadHadNetworkError (const String&) { return true; } | |||
void WebBrowserComponent::windowCloseRequest() {} | |||
void WebBrowserComponent::newWindowAttemptingToLoad (const String&) {} | |||
} | |||
#endif |
@@ -0,0 +1,99 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
/******************************************************************************* | |||
The block below describes the properties of this module, and is read by | |||
the Projucer to automatically generate project code that uses it. | |||
For details about the syntax and how to create or use a module, see the | |||
JUCE Module Format.txt file. | |||
BEGIN_JUCE_MODULE_DECLARATION | |||
ID: juce_gui_extra | |||
vendor: juce | |||
version: 5.1.2 | |||
name: JUCE extended GUI classes | |||
description: Miscellaneous GUI classes for specialised tasks. | |||
website: http://www.juce.com/juce | |||
license: GPL/Commercial | |||
dependencies: juce_gui_basics | |||
OSXFrameworks: WebKit | |||
END_JUCE_MODULE_DECLARATION | |||
*******************************************************************************/ | |||
#pragma once | |||
#define JUCE_GUI_EXTRA_H_INCLUDED | |||
#include <juce_gui_basics/juce_gui_basics.h> | |||
//============================================================================== | |||
/** Config: JUCE_WEB_BROWSER | |||
This lets you disable the WebBrowserComponent class (Mac and Windows). | |||
If you're not using any embedded web-pages, turning this off may reduce your code size. | |||
*/ | |||
#ifndef JUCE_WEB_BROWSER | |||
#define JUCE_WEB_BROWSER 1 | |||
#endif | |||
/** Config: JUCE_ENABLE_LIVE_CONSTANT_EDITOR | |||
This lets you turn on the JUCE_ENABLE_LIVE_CONSTANT_EDITOR support. See the documentation | |||
for that macro for more details. | |||
*/ | |||
#ifndef JUCE_ENABLE_LIVE_CONSTANT_EDITOR | |||
#if JUCE_DEBUG | |||
#define JUCE_ENABLE_LIVE_CONSTANT_EDITOR 1 | |||
#endif | |||
#endif | |||
//============================================================================== | |||
#include "documents/juce_FileBasedDocument.h" | |||
#include "code_editor/juce_CodeDocument.h" | |||
#include "code_editor/juce_CodeEditorComponent.h" | |||
#include "code_editor/juce_CodeTokeniser.h" | |||
#include "code_editor/juce_CPlusPlusCodeTokeniser.h" | |||
#include "code_editor/juce_CPlusPlusCodeTokeniserFunctions.h" | |||
#include "code_editor/juce_XMLCodeTokeniser.h" | |||
#include "code_editor/juce_LuaCodeTokeniser.h" | |||
#include "embedding/juce_ActiveXControlComponent.h" | |||
#include "embedding/juce_NSViewComponent.h" | |||
#include "embedding/juce_UIViewComponent.h" | |||
#include "embedding/juce_XEmbedComponent.h" | |||
#include "misc/juce_AppleRemote.h" | |||
#include "misc/juce_BubbleMessageComponent.h" | |||
#include "misc/juce_ColourSelector.h" | |||
#include "misc/juce_KeyMappingEditorComponent.h" | |||
#include "misc/juce_PreferencesPanel.h" | |||
#include "misc/juce_RecentlyOpenedFilesList.h" | |||
#include "misc/juce_SplashScreen.h" | |||
#include "misc/juce_SystemTrayIconComponent.h" | |||
#include "misc/juce_WebBrowserComponent.h" | |||
#include "misc/juce_LiveConstantEditor.h" | |||
#include "misc/juce_AnimatedAppComponent.h" |
@@ -0,0 +1,55 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
AnimatedAppComponent::AnimatedAppComponent() | |||
: lastUpdateTime (Time::getCurrentTime()), totalUpdates (0) | |||
{ | |||
setOpaque (true); | |||
} | |||
void AnimatedAppComponent::setFramesPerSecond (int framesPerSecond) | |||
{ | |||
jassert (framesPerSecond > 0 && framesPerSecond < 1000); | |||
startTimerHz (framesPerSecond); | |||
} | |||
int AnimatedAppComponent::getMillisecondsSinceLastUpdate() const noexcept | |||
{ | |||
return (int) (Time::getCurrentTime() - lastUpdateTime).inMilliseconds(); | |||
} | |||
void AnimatedAppComponent::timerCallback() | |||
{ | |||
++totalUpdates; | |||
update(); | |||
repaint(); | |||
lastUpdateTime = Time::getCurrentTime(); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,76 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A base class for writing simple one-page graphical apps. | |||
A subclass can inherit from this and implement just a few methods such as | |||
paint() and mouse-handling. The base class provides some simple abstractions | |||
to take care of continuously repainting itself. | |||
*/ | |||
class AnimatedAppComponent : public Component, | |||
private Timer | |||
{ | |||
public: | |||
AnimatedAppComponent(); | |||
/** Your subclass can call this to start a timer running which will | |||
call update() and repaint the component at the given frequency. | |||
*/ | |||
void setFramesPerSecond (int framesPerSecond); | |||
/** Called periodically, at the frequency specified by setFramesPerSecond(). | |||
This is a the best place to do things like advancing animation parameters, | |||
checking the mouse position, etc. | |||
*/ | |||
virtual void update() = 0; | |||
/** Returns the number of times that update() has been called since the component | |||
started running. | |||
*/ | |||
int getFrameCounter() const noexcept { return totalUpdates; } | |||
/** When called from update(), this returns the number of milliseconds since the | |||
last update call. | |||
This might be useful for accurately timing animations, etc. | |||
*/ | |||
int getMillisecondsSinceLastUpdate() const noexcept; | |||
private: | |||
//============================================================================== | |||
Time lastUpdateTime; | |||
int totalUpdates; | |||
void timerCallback() override; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnimatedAppComponent) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,116 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
#if JUCE_MAC || DOXYGEN | |||
/** | |||
Receives events from an Apple IR remote control device (Only available in OSX!). | |||
To use it, just create a subclass of this class, implementing the buttonPressed() | |||
callback, then call start() and stop() to start or stop receiving events. | |||
*/ | |||
class JUCE_API AppleRemoteDevice | |||
{ | |||
public: | |||
//============================================================================== | |||
AppleRemoteDevice(); | |||
virtual ~AppleRemoteDevice(); | |||
//============================================================================== | |||
/** The set of buttons that may be pressed. | |||
@see buttonPressed | |||
*/ | |||
enum ButtonType | |||
{ | |||
menuButton = 0, /**< The menu button (if it's held for a short time). */ | |||
playButton, /**< The play button. */ | |||
plusButton, /**< The plus or volume-up button. */ | |||
minusButton, /**< The minus or volume-down button. */ | |||
rightButton, /**< The right button (if it's held for a short time). */ | |||
leftButton, /**< The left button (if it's held for a short time). */ | |||
rightButton_Long, /**< The right button (if it's held for a long time). */ | |||
leftButton_Long, /**< The menu button (if it's held for a long time). */ | |||
menuButton_Long, /**< The menu button (if it's held for a long time). */ | |||
playButtonSleepMode, | |||
switched | |||
}; | |||
//============================================================================== | |||
/** Override this method to receive the callback about a button press. | |||
The callback will happen on the application's message thread. | |||
Some buttons trigger matching up and down events, in which the isDown parameter | |||
will be true and then false. Others only send a single event when the | |||
button is pressed. | |||
*/ | |||
virtual void buttonPressed (ButtonType buttonId, bool isDown) = 0; | |||
//============================================================================== | |||
/** Starts the device running and responding to events. | |||
Returns true if it managed to open the device. | |||
@param inExclusiveMode if true, the remote will be grabbed exclusively for this app, | |||
and will not be available to any other part of the system. If | |||
false, it will be shared with other apps. | |||
@see stop | |||
*/ | |||
bool start (bool inExclusiveMode); | |||
/** Stops the device running. | |||
@see start | |||
*/ | |||
void stop(); | |||
/** Returns true if the device has been started successfully. | |||
*/ | |||
bool isActive() const; | |||
/** Returns the ID number of the remote, if it has sent one. | |||
*/ | |||
int getRemoteId() const { return remoteId; } | |||
//============================================================================== | |||
/** @internal */ | |||
void handleCallbackInternal(); | |||
private: | |||
void* device; | |||
void* queue; | |||
int remoteId; | |||
bool open (bool openInExclusiveMode); | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AppleRemoteDevice) | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,125 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
BubbleMessageComponent::BubbleMessageComponent (int fadeOutLengthMs) | |||
: fadeOutLength (fadeOutLengthMs), mouseClickCounter (0), | |||
expiryTime (0), deleteAfterUse (false) | |||
{ | |||
} | |||
BubbleMessageComponent::~BubbleMessageComponent() | |||
{ | |||
} | |||
void BubbleMessageComponent::showAt (const Rectangle<int>& pos, | |||
const AttributedString& text, | |||
const int numMillisecondsBeforeRemoving, | |||
const bool removeWhenMouseClicked, | |||
const bool deleteSelfAfterUse) | |||
{ | |||
createLayout (text); | |||
setPosition (pos); | |||
init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); | |||
} | |||
void BubbleMessageComponent::showAt (Component* const component, | |||
const AttributedString& text, | |||
const int numMillisecondsBeforeRemoving, | |||
const bool removeWhenMouseClicked, | |||
const bool deleteSelfAfterUse) | |||
{ | |||
createLayout (text); | |||
setPosition (component); | |||
init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); | |||
} | |||
void BubbleMessageComponent::createLayout (const AttributedString& text) | |||
{ | |||
textLayout.createLayoutWithBalancedLineLengths (text, 256); | |||
} | |||
void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving, | |||
const bool removeWhenMouseClicked, | |||
const bool deleteSelfAfterUse) | |||
{ | |||
setAlpha (1.0f); | |||
setVisible (true); | |||
deleteAfterUse = deleteSelfAfterUse; | |||
expiryTime = numMillisecondsBeforeRemoving > 0 | |||
? (Time::getMillisecondCounter() + (uint32) numMillisecondsBeforeRemoving) : 0; | |||
mouseClickCounter = Desktop::getInstance().getMouseButtonClickCounter(); | |||
if (! (removeWhenMouseClicked && isShowing())) | |||
mouseClickCounter += 0xfffff; | |||
startTimer (77); | |||
repaint(); | |||
} | |||
const float bubblePaddingX = 20.0f; | |||
const float bubblePaddingY = 14.0f; | |||
void BubbleMessageComponent::getContentSize (int& w, int& h) | |||
{ | |||
w = (int) (bubblePaddingX + textLayout.getWidth()); | |||
h = (int) (bubblePaddingY + textLayout.getHeight()); | |||
} | |||
void BubbleMessageComponent::paintContent (Graphics& g, int w, int h) | |||
{ | |||
g.setColour (findColour (TooltipWindow::textColourId)); | |||
textLayout.draw (g, Rectangle<float> (bubblePaddingX / 2.0f, bubblePaddingY / 2.0f, | |||
w - bubblePaddingX, h - bubblePaddingY)); | |||
} | |||
void BubbleMessageComponent::timerCallback() | |||
{ | |||
if (Desktop::getInstance().getMouseButtonClickCounter() > mouseClickCounter) | |||
hide (false); | |||
else if (expiryTime != 0 && Time::getMillisecondCounter() > expiryTime) | |||
hide (true); | |||
} | |||
void BubbleMessageComponent::hide (const bool fadeOut) | |||
{ | |||
stopTimer(); | |||
if (fadeOut) | |||
Desktop::getInstance().getAnimator().fadeOut (this, fadeOutLength); | |||
else | |||
setVisible (false); | |||
if (deleteAfterUse) | |||
delete this; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,130 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A speech-bubble component that displays a short message. | |||
This can be used to show a message with the tail of the speech bubble | |||
pointing to a particular component or location on the screen. | |||
@see BubbleComponent | |||
*/ | |||
class JUCE_API BubbleMessageComponent : public BubbleComponent, | |||
private Timer | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates a bubble component. | |||
After creating one a BubbleComponent, do the following: | |||
- add it to an appropriate parent component, or put it on the | |||
desktop with Component::addToDesktop (0). | |||
- use the showAt() method to show a message. | |||
- it will make itself invisible after it times-out (and can optionally | |||
also delete itself), or you can reuse it somewhere else by calling | |||
showAt() again. | |||
*/ | |||
BubbleMessageComponent (int fadeOutLengthMs = 150); | |||
/** Destructor. */ | |||
~BubbleMessageComponent(); | |||
//============================================================================== | |||
/** Shows a message bubble at a particular position. | |||
This shows the bubble with its stem pointing to the given location | |||
(coordinates being relative to its parent component). | |||
For details about exactly how it decides where to position itself, see | |||
BubbleComponent::updatePosition(). | |||
@param position the coords of the object to point to | |||
@param message the text to display | |||
@param numMillisecondsBeforeRemoving how long to leave it on the screen before removing itself | |||
from its parent compnent. If this is 0 or less, it | |||
will stay there until manually removed. | |||
@param removeWhenMouseClicked if this is true, the bubble will disappear as soon as a | |||
mouse button is pressed (anywhere on the screen) | |||
@param deleteSelfAfterUse if true, then the component will delete itself after | |||
it becomes invisible | |||
*/ | |||
void showAt (const Rectangle<int>& position, | |||
const AttributedString& message, | |||
int numMillisecondsBeforeRemoving, | |||
bool removeWhenMouseClicked = true, | |||
bool deleteSelfAfterUse = false); | |||
/** Shows a message bubble next to a particular component. | |||
This shows the bubble with its stem pointing at the given component. | |||
For details about exactly how it decides where to position itself, see | |||
BubbleComponent::updatePosition(). | |||
@param component the component that you want to point at | |||
@param message the text to display | |||
@param numMillisecondsBeforeRemoving how long to leave it on the screen before removing itself | |||
from its parent compnent. If this is 0 or less, it | |||
will stay there until manually removed. | |||
@param removeWhenMouseClicked if this is true, the bubble will disappear as soon as a | |||
mouse button is pressed (anywhere on the screen) | |||
@param deleteSelfAfterUse if true, then the component will delete itself after | |||
it becomes invisible | |||
*/ | |||
void showAt (Component* component, | |||
const AttributedString& message, | |||
int numMillisecondsBeforeRemoving, | |||
bool removeWhenMouseClicked = true, | |||
bool deleteSelfAfterUse = false); | |||
//============================================================================== | |||
/** @internal */ | |||
void getContentSize (int& w, int& h) override; | |||
/** @internal */ | |||
void paintContent (Graphics& g, int w, int h) override; | |||
/** @internal */ | |||
void timerCallback() override; | |||
private: | |||
//============================================================================== | |||
int fadeOutLength, mouseClickCounter; | |||
TextLayout textLayout; | |||
int64 expiryTime; | |||
bool deleteAfterUse; | |||
void createLayout (const AttributedString&); | |||
void init (int, bool, bool); | |||
void hide (bool fadeOut); | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BubbleMessageComponent) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,575 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class ColourSelector::ColourComponentSlider : public Slider | |||
{ | |||
public: | |||
ColourComponentSlider (const String& name) | |||
: Slider (name) | |||
{ | |||
setRange (0.0, 255.0, 1.0); | |||
} | |||
String getTextFromValue (double value) | |||
{ | |||
return String::toHexString ((int) value).toUpperCase().paddedLeft ('0', 2); | |||
} | |||
double getValueFromText (const String& text) | |||
{ | |||
return (double) text.getHexValue32(); | |||
} | |||
JUCE_DECLARE_NON_COPYABLE (ColourComponentSlider) | |||
}; | |||
//============================================================================== | |||
class ColourSelector::ColourSpaceMarker : public Component | |||
{ | |||
public: | |||
ColourSpaceMarker() | |||
{ | |||
setInterceptsMouseClicks (false, false); | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
g.setColour (Colour::greyLevel (0.1f)); | |||
g.drawEllipse (1.0f, 1.0f, getWidth() - 2.0f, getHeight() - 2.0f, 1.0f); | |||
g.setColour (Colour::greyLevel (0.9f)); | |||
g.drawEllipse (2.0f, 2.0f, getWidth() - 4.0f, getHeight() - 4.0f, 1.0f); | |||
} | |||
JUCE_DECLARE_NON_COPYABLE (ColourSpaceMarker) | |||
}; | |||
//============================================================================== | |||
class ColourSelector::ColourSpaceView : public Component | |||
{ | |||
public: | |||
ColourSpaceView (ColourSelector& cs, float& hue, float& sat, float& val, const int edgeSize) | |||
: owner (cs), h (hue), s (sat), v (val), lastHue (0.0f), edge (edgeSize) | |||
{ | |||
addAndMakeVisible (marker); | |||
setMouseCursor (MouseCursor::CrosshairCursor); | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
if (colours.isNull()) | |||
{ | |||
const int width = getWidth() / 2; | |||
const int height = getHeight() / 2; | |||
colours = Image (Image::RGB, width, height, false); | |||
Image::BitmapData pixels (colours, Image::BitmapData::writeOnly); | |||
for (int y = 0; y < height; ++y) | |||
{ | |||
const float val = 1.0f - y / (float) height; | |||
for (int x = 0; x < width; ++x) | |||
{ | |||
const float sat = x / (float) width; | |||
pixels.setPixelColour (x, y, Colour (h, sat, val, 1.0f)); | |||
} | |||
} | |||
} | |||
g.setOpacity (1.0f); | |||
g.drawImageTransformed (colours, | |||
RectanglePlacement (RectanglePlacement::stretchToFit) | |||
.getTransformToFit (colours.getBounds().toFloat(), | |||
getLocalBounds().reduced (edge).toFloat()), | |||
false); | |||
} | |||
void mouseDown (const MouseEvent& e) override | |||
{ | |||
mouseDrag (e); | |||
} | |||
void mouseDrag (const MouseEvent& e) override | |||
{ | |||
const float sat = (e.x - edge) / (float) (getWidth() - edge * 2); | |||
const float val = 1.0f - (e.y - edge) / (float) (getHeight() - edge * 2); | |||
owner.setSV (sat, val); | |||
} | |||
void updateIfNeeded() | |||
{ | |||
if (lastHue != h) | |||
{ | |||
lastHue = h; | |||
colours = Image(); | |||
repaint(); | |||
} | |||
updateMarker(); | |||
} | |||
void resized() override | |||
{ | |||
colours = Image(); | |||
updateMarker(); | |||
} | |||
private: | |||
ColourSelector& owner; | |||
float& h; | |||
float& s; | |||
float& v; | |||
float lastHue; | |||
ColourSpaceMarker marker; | |||
const int edge; | |||
Image colours; | |||
void updateMarker() | |||
{ | |||
marker.setBounds (roundToInt ((getWidth() - edge * 2) * s), | |||
roundToInt ((getHeight() - edge * 2) * (1.0f - v)), | |||
edge * 2, edge * 2); | |||
} | |||
JUCE_DECLARE_NON_COPYABLE (ColourSpaceView) | |||
}; | |||
//============================================================================== | |||
class ColourSelector::HueSelectorMarker : public Component | |||
{ | |||
public: | |||
HueSelectorMarker() | |||
{ | |||
setInterceptsMouseClicks (false, false); | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
const float cw = (float) getWidth(); | |||
const float ch = (float) getHeight(); | |||
Path p; | |||
p.addTriangle (1.0f, 1.0f, | |||
cw * 0.3f, ch * 0.5f, | |||
1.0f, ch - 1.0f); | |||
p.addTriangle (cw - 1.0f, 1.0f, | |||
cw * 0.7f, ch * 0.5f, | |||
cw - 1.0f, ch - 1.0f); | |||
g.setColour (Colours::white.withAlpha (0.75f)); | |||
g.fillPath (p); | |||
g.setColour (Colours::black.withAlpha (0.75f)); | |||
g.strokePath (p, PathStrokeType (1.2f)); | |||
} | |||
private: | |||
JUCE_DECLARE_NON_COPYABLE (HueSelectorMarker) | |||
}; | |||
//============================================================================== | |||
class ColourSelector::HueSelectorComp : public Component | |||
{ | |||
public: | |||
HueSelectorComp (ColourSelector& cs, float& hue, const int edgeSize) | |||
: owner (cs), h (hue), edge (edgeSize) | |||
{ | |||
addAndMakeVisible (marker); | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
ColourGradient cg; | |||
cg.isRadial = false; | |||
cg.point1.setXY (0.0f, (float) edge); | |||
cg.point2.setXY (0.0f, (float) getHeight()); | |||
for (float i = 0.0f; i <= 1.0f; i += 0.02f) | |||
cg.addColour (i, Colour (i, 1.0f, 1.0f, 1.0f)); | |||
g.setGradientFill (cg); | |||
g.fillRect (getLocalBounds().reduced (edge)); | |||
} | |||
void resized() override | |||
{ | |||
marker.setBounds (0, roundToInt ((getHeight() - edge * 2) * h), getWidth(), edge * 2); | |||
} | |||
void mouseDown (const MouseEvent& e) override | |||
{ | |||
mouseDrag (e); | |||
} | |||
void mouseDrag (const MouseEvent& e) override | |||
{ | |||
owner.setHue ((e.y - edge) / (float) (getHeight() - edge * 2)); | |||
} | |||
void updateIfNeeded() | |||
{ | |||
resized(); | |||
} | |||
private: | |||
ColourSelector& owner; | |||
float& h; | |||
HueSelectorMarker marker; | |||
const int edge; | |||
JUCE_DECLARE_NON_COPYABLE (HueSelectorComp) | |||
}; | |||
//============================================================================== | |||
class ColourSelector::SwatchComponent : public Component | |||
{ | |||
public: | |||
SwatchComponent (ColourSelector& cs, int itemIndex) | |||
: owner (cs), index (itemIndex) | |||
{ | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
const Colour c (owner.getSwatchColour (index)); | |||
g.fillCheckerBoard (getLocalBounds(), 6, 6, | |||
Colour (0xffdddddd).overlaidWith (c), | |||
Colour (0xffffffff).overlaidWith (c)); | |||
} | |||
void mouseDown (const MouseEvent&) override | |||
{ | |||
PopupMenu m; | |||
m.addItem (1, TRANS("Use this swatch as the current colour")); | |||
m.addSeparator(); | |||
m.addItem (2, TRANS("Set this swatch to the current colour")); | |||
m.showMenuAsync (PopupMenu::Options().withTargetComponent (this), | |||
ModalCallbackFunction::forComponent (menuStaticCallback, this)); | |||
} | |||
private: | |||
ColourSelector& owner; | |||
const int index; | |||
static void menuStaticCallback (int result, SwatchComponent* comp) | |||
{ | |||
if (comp != nullptr) | |||
{ | |||
if (result == 1) | |||
comp->setColourFromSwatch(); | |||
else if (result == 2) | |||
comp->setSwatchFromColour(); | |||
} | |||
} | |||
void setColourFromSwatch() | |||
{ | |||
owner.setCurrentColour (owner.getSwatchColour (index)); | |||
} | |||
void setSwatchFromColour() | |||
{ | |||
if (owner.getSwatchColour (index) != owner.getCurrentColour()) | |||
{ | |||
owner.setSwatchColour (index, owner.getCurrentColour()); | |||
repaint(); | |||
} | |||
} | |||
JUCE_DECLARE_NON_COPYABLE (SwatchComponent) | |||
}; | |||
//============================================================================== | |||
ColourSelector::ColourSelector (const int sectionsToShow, const int edge, const int gapAroundColourSpaceComponent) | |||
: colour (Colours::white), | |||
flags (sectionsToShow), | |||
edgeGap (edge) | |||
{ | |||
// not much point having a selector with no components in it! | |||
jassert ((flags & (showColourAtTop | showSliders | showColourspace)) != 0); | |||
updateHSV(); | |||
if ((flags & showSliders) != 0) | |||
{ | |||
addAndMakeVisible (sliders[0] = new ColourComponentSlider (TRANS ("red"))); | |||
addAndMakeVisible (sliders[1] = new ColourComponentSlider (TRANS ("green"))); | |||
addAndMakeVisible (sliders[2] = new ColourComponentSlider (TRANS ("blue"))); | |||
addChildComponent (sliders[3] = new ColourComponentSlider (TRANS ("alpha"))); | |||
sliders[3]->setVisible ((flags & showAlphaChannel) != 0); | |||
for (int i = 4; --i >= 0;) | |||
sliders[i]->addListener (this); | |||
} | |||
if ((flags & showColourspace) != 0) | |||
{ | |||
addAndMakeVisible (colourSpace = new ColourSpaceView (*this, h, s, v, gapAroundColourSpaceComponent)); | |||
addAndMakeVisible (hueSelector = new HueSelectorComp (*this, h, gapAroundColourSpaceComponent)); | |||
} | |||
update (dontSendNotification); | |||
} | |||
ColourSelector::~ColourSelector() | |||
{ | |||
dispatchPendingMessages(); | |||
swatchComponents.clear(); | |||
} | |||
//============================================================================== | |||
Colour ColourSelector::getCurrentColour() const | |||
{ | |||
return ((flags & showAlphaChannel) != 0) ? colour : colour.withAlpha ((uint8) 0xff); | |||
} | |||
void ColourSelector::setCurrentColour (Colour c, NotificationType notification) | |||
{ | |||
if (c != colour) | |||
{ | |||
colour = ((flags & showAlphaChannel) != 0) ? c : c.withAlpha ((uint8) 0xff); | |||
updateHSV(); | |||
update (notification); | |||
} | |||
} | |||
void ColourSelector::setHue (float newH) | |||
{ | |||
newH = jlimit (0.0f, 1.0f, newH); | |||
if (h != newH) | |||
{ | |||
h = newH; | |||
colour = Colour (h, s, v, colour.getFloatAlpha()); | |||
update (sendNotification); | |||
} | |||
} | |||
void ColourSelector::setSV (float newS, float newV) | |||
{ | |||
newS = jlimit (0.0f, 1.0f, newS); | |||
newV = jlimit (0.0f, 1.0f, newV); | |||
if (s != newS || v != newV) | |||
{ | |||
s = newS; | |||
v = newV; | |||
colour = Colour (h, s, v, colour.getFloatAlpha()); | |||
update (sendNotification); | |||
} | |||
} | |||
//============================================================================== | |||
void ColourSelector::updateHSV() | |||
{ | |||
colour.getHSB (h, s, v); | |||
} | |||
void ColourSelector::update (NotificationType notification) | |||
{ | |||
if (sliders[0] != nullptr) | |||
{ | |||
sliders[0]->setValue ((int) colour.getRed(), notification); | |||
sliders[1]->setValue ((int) colour.getGreen(), notification); | |||
sliders[2]->setValue ((int) colour.getBlue(), notification); | |||
sliders[3]->setValue ((int) colour.getAlpha(), notification); | |||
} | |||
if (colourSpace != nullptr) | |||
{ | |||
colourSpace->updateIfNeeded(); | |||
hueSelector->updateIfNeeded(); | |||
} | |||
if ((flags & showColourAtTop) != 0) | |||
repaint (previewArea); | |||
if (notification != dontSendNotification) | |||
sendChangeMessage(); | |||
if (notification == sendNotificationSync) | |||
dispatchPendingMessages(); | |||
} | |||
//============================================================================== | |||
void ColourSelector::paint (Graphics& g) | |||
{ | |||
g.fillAll (findColour (backgroundColourId)); | |||
if ((flags & showColourAtTop) != 0) | |||
{ | |||
const Colour currentColour (getCurrentColour()); | |||
g.fillCheckerBoard (previewArea, 10, 10, | |||
Colour (0xffdddddd).overlaidWith (currentColour), | |||
Colour (0xffffffff).overlaidWith (currentColour)); | |||
g.setColour (Colours::white.overlaidWith (currentColour).contrasting()); | |||
g.setFont (Font (14.0f, Font::bold)); | |||
g.drawText (currentColour.toDisplayString ((flags & showAlphaChannel) != 0), | |||
previewArea, Justification::centred, false); | |||
} | |||
if ((flags & showSliders) != 0) | |||
{ | |||
g.setColour (findColour (labelTextColourId)); | |||
g.setFont (11.0f); | |||
for (int i = 4; --i >= 0;) | |||
{ | |||
if (sliders[i]->isVisible()) | |||
g.drawText (sliders[i]->getName() + ":", | |||
0, sliders[i]->getY(), | |||
sliders[i]->getX() - 8, sliders[i]->getHeight(), | |||
Justification::centredRight, false); | |||
} | |||
} | |||
} | |||
void ColourSelector::resized() | |||
{ | |||
const int swatchesPerRow = 8; | |||
const int swatchHeight = 22; | |||
const int numSliders = ((flags & showAlphaChannel) != 0) ? 4 : 3; | |||
const int numSwatches = getNumSwatches(); | |||
const int swatchSpace = numSwatches > 0 ? edgeGap + swatchHeight * ((numSwatches + 7) / swatchesPerRow) : 0; | |||
const int sliderSpace = ((flags & showSliders) != 0) ? jmin (22 * numSliders + edgeGap, proportionOfHeight (0.3f)) : 0; | |||
const int topSpace = ((flags & showColourAtTop) != 0) ? jmin (30 + edgeGap * 2, proportionOfHeight (0.2f)) : edgeGap; | |||
previewArea.setBounds (edgeGap, edgeGap, getWidth() - edgeGap * 2, topSpace - edgeGap * 2); | |||
int y = topSpace; | |||
if ((flags & showColourspace) != 0) | |||
{ | |||
const int hueWidth = jmin (50, proportionOfWidth (0.15f)); | |||
colourSpace->setBounds (edgeGap, y, | |||
getWidth() - hueWidth - edgeGap - 4, | |||
getHeight() - topSpace - sliderSpace - swatchSpace - edgeGap); | |||
hueSelector->setBounds (colourSpace->getRight() + 4, y, | |||
getWidth() - edgeGap - (colourSpace->getRight() + 4), | |||
colourSpace->getHeight()); | |||
y = getHeight() - sliderSpace - swatchSpace - edgeGap; | |||
} | |||
if ((flags & showSliders) != 0) | |||
{ | |||
const int sliderHeight = jmax (4, sliderSpace / numSliders); | |||
for (int i = 0; i < numSliders; ++i) | |||
{ | |||
sliders[i]->setBounds (proportionOfWidth (0.2f), y, | |||
proportionOfWidth (0.72f), sliderHeight - 2); | |||
y += sliderHeight; | |||
} | |||
} | |||
if (numSwatches > 0) | |||
{ | |||
const int startX = 8; | |||
const int xGap = 4; | |||
const int yGap = 4; | |||
const int swatchWidth = (getWidth() - startX * 2) / swatchesPerRow; | |||
y += edgeGap; | |||
if (swatchComponents.size() != numSwatches) | |||
{ | |||
swatchComponents.clear(); | |||
for (int i = 0; i < numSwatches; ++i) | |||
{ | |||
auto* sc = new SwatchComponent (*this, i); | |||
swatchComponents.add (sc); | |||
addAndMakeVisible (sc); | |||
} | |||
} | |||
int x = startX; | |||
for (int i = 0; i < swatchComponents.size(); ++i) | |||
{ | |||
auto* sc = swatchComponents.getUnchecked(i); | |||
sc->setBounds (x + xGap / 2, | |||
y + yGap / 2, | |||
swatchWidth - xGap, | |||
swatchHeight - yGap); | |||
if (((i + 1) % swatchesPerRow) == 0) | |||
{ | |||
x = startX; | |||
y += swatchHeight; | |||
} | |||
else | |||
{ | |||
x += swatchWidth; | |||
} | |||
} | |||
} | |||
} | |||
void ColourSelector::sliderValueChanged (Slider*) | |||
{ | |||
if (sliders[0] != nullptr) | |||
setCurrentColour (Colour ((uint8) sliders[0]->getValue(), | |||
(uint8) sliders[1]->getValue(), | |||
(uint8) sliders[2]->getValue(), | |||
(uint8) sliders[3]->getValue())); | |||
} | |||
//============================================================================== | |||
int ColourSelector::getNumSwatches() const | |||
{ | |||
return 0; | |||
} | |||
Colour ColourSelector::getSwatchColour (const int) const | |||
{ | |||
jassertfalse; // if you've overridden getNumSwatches(), you also need to implement this method | |||
return Colours::black; | |||
} | |||
void ColourSelector::setSwatchColour (int, const Colour&) | |||
{ | |||
jassertfalse; // if you've overridden getNumSwatches(), you also need to implement this method | |||
} | |||
} // namespace juce |
@@ -0,0 +1,171 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A component that lets the user choose a colour. | |||
This shows RGB sliders and a colourspace that the user can pick colours from. | |||
This class is also a ChangeBroadcaster, so listeners can register to be told | |||
when the colour changes. | |||
*/ | |||
class JUCE_API ColourSelector : public Component, | |||
public ChangeBroadcaster, | |||
protected Slider::Listener | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Options for the type of selector to show. These are passed into the constructor. */ | |||
enum ColourSelectorOptions | |||
{ | |||
showAlphaChannel = 1 << 0, /**< if set, the colour's alpha channel can be changed as well as its RGB. */ | |||
showColourAtTop = 1 << 1, /**< if set, a swatch of the colour is shown at the top of the component. */ | |||
showSliders = 1 << 2, /**< if set, RGB sliders are shown at the bottom of the component. */ | |||
showColourspace = 1 << 3 /**< if set, a big HSV selector is shown. */ | |||
}; | |||
//============================================================================== | |||
/** Creates a ColourSelector object. | |||
The flags are a combination of values from the ColourSelectorOptions enum, specifying | |||
which of the selector's features should be visible. | |||
The edgeGap value specifies the amount of space to leave around the edge. | |||
gapAroundColourSpaceComponent indicates how much of a gap to put around the | |||
colourspace and hue selector components. | |||
*/ | |||
ColourSelector (int flags = (showAlphaChannel | showColourAtTop | showSliders | showColourspace), | |||
int edgeGap = 4, | |||
int gapAroundColourSpaceComponent = 7); | |||
/** Destructor. */ | |||
~ColourSelector(); | |||
//============================================================================== | |||
/** Returns the colour that the user has currently selected. | |||
The ColourSelector class is also a ChangeBroadcaster, so listeners can | |||
register to be told when the colour changes. | |||
@see setCurrentColour | |||
*/ | |||
Colour getCurrentColour() const; | |||
/** Changes the colour that is currently being shown. */ | |||
void setCurrentColour (Colour newColour, NotificationType notificationType = sendNotification); | |||
//============================================================================== | |||
/** Tells the selector how many preset colour swatches you want to have on the component. | |||
To enable swatches, you'll need to override getNumSwatches(), getSwatchColour(), and | |||
setSwatchColour(), to return the number of colours you want, and to set and retrieve | |||
their values. | |||
*/ | |||
virtual int getNumSwatches() const; | |||
/** Called by the selector to find out the colour of one of the swatches. | |||
Your subclass should return the colour of the swatch with the given index. | |||
To enable swatches, you'll need to override getNumSwatches(), getSwatchColour(), and | |||
setSwatchColour(), to return the number of colours you want, and to set and retrieve | |||
their values. | |||
*/ | |||
virtual Colour getSwatchColour (int index) const; | |||
/** Called by the selector when the user puts a new colour into one of the swatches. | |||
Your subclass should change the colour of the swatch with the given index. | |||
To enable swatches, you'll need to override getNumSwatches(), getSwatchColour(), and | |||
setSwatchColour(), to return the number of colours you want, and to set and retrieve | |||
their values. | |||
*/ | |||
virtual void setSwatchColour (int index, const Colour& newColour); | |||
//============================================================================== | |||
/** A set of colour IDs to use to change the colour of various aspects of the keyboard. | |||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() | |||
methods. | |||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour | |||
*/ | |||
enum ColourIds | |||
{ | |||
backgroundColourId = 0x1007000, /**< the colour used to fill the component's background. */ | |||
labelTextColourId = 0x1007001 /**< the colour used for the labels next to the sliders. */ | |||
}; | |||
private: | |||
//============================================================================== | |||
class ColourSpaceView; | |||
class HueSelectorComp; | |||
class SwatchComponent; | |||
class ColourComponentSlider; | |||
class ColourSpaceMarker; | |||
class HueSelectorMarker; | |||
friend class ColourSpaceView; | |||
friend struct ContainerDeletePolicy<ColourSpaceView>; | |||
friend class HueSelectorComp; | |||
friend struct ContainerDeletePolicy<HueSelectorComp>; | |||
Colour colour; | |||
float h, s, v; | |||
ScopedPointer<Slider> sliders[4]; | |||
ScopedPointer<ColourSpaceView> colourSpace; | |||
ScopedPointer<HueSelectorComp> hueSelector; | |||
OwnedArray<SwatchComponent> swatchComponents; | |||
const int flags; | |||
int edgeGap; | |||
Rectangle<int> previewArea; | |||
void setHue (float newH); | |||
void setSV (float newS, float newV); | |||
void updateHSV(); | |||
void update (NotificationType); | |||
void sliderValueChanged (Slider*) override; | |||
void paint (Graphics&) override; | |||
void resized() override; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ColourSelector) | |||
#if JUCE_CATCH_DEPRECATED_CODE_MISUSE | |||
// This constructor is here temporarily to prevent old code compiling, because the parameters | |||
// have changed - if you get an error here, update your code to use the new constructor instead.. | |||
ColourSelector (bool); | |||
#endif | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,475 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class KeyMappingEditorComponent::ChangeKeyButton : public Button | |||
{ | |||
public: | |||
ChangeKeyButton (KeyMappingEditorComponent& kec, const CommandID command, | |||
const String& keyName, const int keyIndex) | |||
: Button (keyName), | |||
owner (kec), | |||
commandID (command), | |||
keyNum (keyIndex) | |||
{ | |||
setWantsKeyboardFocus (false); | |||
setTriggeredOnMouseDown (keyNum >= 0); | |||
setTooltip (keyIndex < 0 ? TRANS("Adds a new key-mapping") | |||
: TRANS("Click to change this key-mapping")); | |||
} | |||
void paintButton (Graphics& g, bool /*isOver*/, bool /*isDown*/) override | |||
{ | |||
getLookAndFeel().drawKeymapChangeButton (g, getWidth(), getHeight(), *this, | |||
keyNum >= 0 ? getName() : String()); | |||
} | |||
static void menuCallback (int result, ChangeKeyButton* button) | |||
{ | |||
if (button != nullptr) | |||
{ | |||
switch (result) | |||
{ | |||
case 1: button->assignNewKey(); break; | |||
case 2: button->owner.getMappings().removeKeyPress (button->commandID, button->keyNum); break; | |||
default: break; | |||
} | |||
} | |||
} | |||
void clicked() override | |||
{ | |||
if (keyNum >= 0) | |||
{ | |||
// existing key clicked.. | |||
PopupMenu m; | |||
m.addItem (1, TRANS("Change this key-mapping")); | |||
m.addSeparator(); | |||
m.addItem (2, TRANS("Remove this key-mapping")); | |||
m.showMenuAsync (PopupMenu::Options(), | |||
ModalCallbackFunction::forComponent (menuCallback, this)); | |||
} | |||
else | |||
{ | |||
assignNewKey(); // + button pressed.. | |||
} | |||
} | |||
void fitToContent (const int h) noexcept | |||
{ | |||
if (keyNum < 0) | |||
setSize (h, h); | |||
else | |||
setSize (jlimit (h * 4, h * 8, 6 + Font (h * 0.6f).getStringWidth (getName())), h); | |||
} | |||
//============================================================================== | |||
class KeyEntryWindow : public AlertWindow | |||
{ | |||
public: | |||
KeyEntryWindow (KeyMappingEditorComponent& kec) | |||
: AlertWindow (TRANS("New key-mapping"), | |||
TRANS("Please press a key combination now..."), | |||
AlertWindow::NoIcon), | |||
owner (kec) | |||
{ | |||
addButton (TRANS("OK"), 1); | |||
addButton (TRANS("Cancel"), 0); | |||
// (avoid return + escape keys getting processed by the buttons..) | |||
for (auto* child : getChildren()) | |||
child->setWantsKeyboardFocus (false); | |||
setWantsKeyboardFocus (true); | |||
grabKeyboardFocus(); | |||
} | |||
bool keyPressed (const KeyPress& key) override | |||
{ | |||
lastPress = key; | |||
String message (TRANS("Key") + ": " + owner.getDescriptionForKeyPress (key)); | |||
auto previousCommand = owner.getMappings().findCommandForKeyPress (key); | |||
if (previousCommand != 0) | |||
message << "\n\n(" | |||
<< TRANS("Currently assigned to \"CMDN\"") | |||
.replace ("CMDN", TRANS (owner.getCommandManager().getNameOfCommand (previousCommand))) | |||
<< ')'; | |||
setMessage (message); | |||
return true; | |||
} | |||
bool keyStateChanged (bool) override | |||
{ | |||
return true; | |||
} | |||
KeyPress lastPress; | |||
private: | |||
KeyMappingEditorComponent& owner; | |||
JUCE_DECLARE_NON_COPYABLE (KeyEntryWindow) | |||
}; | |||
static void assignNewKeyCallback (int result, ChangeKeyButton* button, KeyPress newKey) | |||
{ | |||
if (result != 0 && button != nullptr) | |||
button->setNewKey (newKey, true); | |||
} | |||
void setNewKey (const KeyPress& newKey, bool dontAskUser) | |||
{ | |||
if (newKey.isValid()) | |||
{ | |||
auto previousCommand = owner.getMappings().findCommandForKeyPress (newKey); | |||
if (previousCommand == 0 || dontAskUser) | |||
{ | |||
owner.getMappings().removeKeyPress (newKey); | |||
if (keyNum >= 0) | |||
owner.getMappings().removeKeyPress (commandID, keyNum); | |||
owner.getMappings().addKeyPress (commandID, newKey, keyNum); | |||
} | |||
else | |||
{ | |||
AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, | |||
TRANS("Change key-mapping"), | |||
TRANS("This key is already assigned to the command \"CMDN\"") | |||
.replace ("CMDN", owner.getCommandManager().getNameOfCommand (previousCommand)) | |||
+ "\n\n" | |||
+ TRANS("Do you want to re-assign it to this new command instead?"), | |||
TRANS("Re-assign"), | |||
TRANS("Cancel"), | |||
this, | |||
ModalCallbackFunction::forComponent (assignNewKeyCallback, | |||
this, KeyPress (newKey))); | |||
} | |||
} | |||
} | |||
static void keyChosen (int result, ChangeKeyButton* button) | |||
{ | |||
if (button != nullptr && button->currentKeyEntryWindow != nullptr) | |||
{ | |||
if (result != 0) | |||
{ | |||
button->currentKeyEntryWindow->setVisible (false); | |||
button->setNewKey (button->currentKeyEntryWindow->lastPress, false); | |||
} | |||
button->currentKeyEntryWindow = nullptr; | |||
} | |||
} | |||
void assignNewKey() | |||
{ | |||
currentKeyEntryWindow = new KeyEntryWindow (owner); | |||
currentKeyEntryWindow->enterModalState (true, ModalCallbackFunction::forComponent (keyChosen, this)); | |||
} | |||
private: | |||
KeyMappingEditorComponent& owner; | |||
const CommandID commandID; | |||
const int keyNum; | |||
ScopedPointer<KeyEntryWindow> currentKeyEntryWindow; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChangeKeyButton) | |||
}; | |||
//============================================================================== | |||
class KeyMappingEditorComponent::ItemComponent : public Component | |||
{ | |||
public: | |||
ItemComponent (KeyMappingEditorComponent& kec, CommandID command) | |||
: owner (kec), commandID (command) | |||
{ | |||
setInterceptsMouseClicks (false, true); | |||
const bool isReadOnly = owner.isCommandReadOnly (commandID); | |||
auto keyPresses = owner.getMappings().getKeyPressesAssignedToCommand (commandID); | |||
for (int i = 0; i < jmin ((int) maxNumAssignments, keyPresses.size()); ++i) | |||
addKeyPressButton (owner.getDescriptionForKeyPress (keyPresses.getReference (i)), i, isReadOnly); | |||
addKeyPressButton (String(), -1, isReadOnly); | |||
} | |||
void addKeyPressButton (const String& desc, const int index, const bool isReadOnly) | |||
{ | |||
auto* b = new ChangeKeyButton (owner, commandID, desc, index); | |||
keyChangeButtons.add (b); | |||
b->setEnabled (! isReadOnly); | |||
b->setVisible (keyChangeButtons.size() <= (int) maxNumAssignments); | |||
addChildComponent (b); | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
g.setFont (getHeight() * 0.7f); | |||
g.setColour (owner.findColour (KeyMappingEditorComponent::textColourId)); | |||
g.drawFittedText (TRANS (owner.getCommandManager().getNameOfCommand (commandID)), | |||
4, 0, jmax (40, getChildComponent (0)->getX() - 5), getHeight(), | |||
Justification::centredLeft, true); | |||
} | |||
void resized() override | |||
{ | |||
int x = getWidth() - 4; | |||
for (int i = keyChangeButtons.size(); --i >= 0;) | |||
{ | |||
auto* b = keyChangeButtons.getUnchecked(i); | |||
b->fitToContent (getHeight() - 2); | |||
b->setTopRightPosition (x, 1); | |||
x = b->getX() - 5; | |||
} | |||
} | |||
private: | |||
KeyMappingEditorComponent& owner; | |||
OwnedArray<ChangeKeyButton> keyChangeButtons; | |||
const CommandID commandID; | |||
enum { maxNumAssignments = 3 }; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent) | |||
}; | |||
//============================================================================== | |||
class KeyMappingEditorComponent::MappingItem : public TreeViewItem | |||
{ | |||
public: | |||
MappingItem (KeyMappingEditorComponent& kec, CommandID command) | |||
: owner (kec), commandID (command) | |||
{} | |||
String getUniqueName() const override { return String ((int) commandID) + "_id"; } | |||
bool mightContainSubItems() override { return false; } | |||
int getItemHeight() const override { return 20; } | |||
Component* createItemComponent() override { return new ItemComponent (owner, commandID); } | |||
private: | |||
KeyMappingEditorComponent& owner; | |||
const CommandID commandID; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingItem) | |||
}; | |||
//============================================================================== | |||
class KeyMappingEditorComponent::CategoryItem : public TreeViewItem | |||
{ | |||
public: | |||
CategoryItem (KeyMappingEditorComponent& kec, const String& name) | |||
: owner (kec), categoryName (name) | |||
{} | |||
String getUniqueName() const override { return categoryName + "_cat"; } | |||
bool mightContainSubItems() override { return true; } | |||
int getItemHeight() const override { return 22; } | |||
void paintItem (Graphics& g, int width, int height) override | |||
{ | |||
g.setFont (Font (height * 0.7f, Font::bold)); | |||
g.setColour (owner.findColour (KeyMappingEditorComponent::textColourId)); | |||
g.drawText (TRANS (categoryName), 2, 0, width - 2, height, Justification::centredLeft, true); | |||
} | |||
void itemOpennessChanged (bool isNowOpen) override | |||
{ | |||
if (isNowOpen) | |||
{ | |||
if (getNumSubItems() == 0) | |||
for (auto command : owner.getCommandManager().getCommandsInCategory (categoryName)) | |||
if (owner.shouldCommandBeIncluded (command)) | |||
addSubItem (new MappingItem (owner, command)); | |||
} | |||
else | |||
{ | |||
clearSubItems(); | |||
} | |||
} | |||
private: | |||
KeyMappingEditorComponent& owner; | |||
String categoryName; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CategoryItem) | |||
}; | |||
//============================================================================== | |||
class KeyMappingEditorComponent::TopLevelItem : public TreeViewItem, | |||
public Button::Listener, | |||
private ChangeListener | |||
{ | |||
public: | |||
TopLevelItem (KeyMappingEditorComponent& kec) : owner (kec) | |||
{ | |||
setLinesDrawnForSubItems (false); | |||
owner.getMappings().addChangeListener (this); | |||
} | |||
~TopLevelItem() | |||
{ | |||
owner.getMappings().removeChangeListener (this); | |||
} | |||
bool mightContainSubItems() override { return true; } | |||
String getUniqueName() const override { return "keys"; } | |||
void changeListenerCallback (ChangeBroadcaster*) override | |||
{ | |||
const OpennessRestorer opennessRestorer (*this); | |||
clearSubItems(); | |||
for (auto category : owner.getCommandManager().getCommandCategories()) | |||
{ | |||
int count = 0; | |||
for (auto command : owner.getCommandManager().getCommandsInCategory (category)) | |||
if (owner.shouldCommandBeIncluded (command)) | |||
++count; | |||
if (count > 0) | |||
addSubItem (new CategoryItem (owner, category)); | |||
} | |||
} | |||
static void resetToDefaultsCallback (int result, KeyMappingEditorComponent* owner) | |||
{ | |||
if (result != 0 && owner != nullptr) | |||
owner->getMappings().resetToDefaultMappings(); | |||
} | |||
void buttonClicked (Button*) override | |||
{ | |||
AlertWindow::showOkCancelBox (AlertWindow::QuestionIcon, | |||
TRANS("Reset to defaults"), | |||
TRANS("Are you sure you want to reset all the key-mappings to their default state?"), | |||
TRANS("Reset"), | |||
String(), | |||
&owner, | |||
ModalCallbackFunction::forComponent (resetToDefaultsCallback, &owner)); | |||
} | |||
private: | |||
KeyMappingEditorComponent& owner; | |||
}; | |||
//============================================================================== | |||
KeyMappingEditorComponent::KeyMappingEditorComponent (KeyPressMappingSet& mappingManager, | |||
const bool showResetToDefaultButton) | |||
: mappings (mappingManager), | |||
resetButton (TRANS ("reset to defaults")) | |||
{ | |||
treeItem = new TopLevelItem (*this); | |||
if (showResetToDefaultButton) | |||
{ | |||
addAndMakeVisible (resetButton); | |||
resetButton.addListener (treeItem); | |||
} | |||
addAndMakeVisible (tree); | |||
tree.setColour (TreeView::backgroundColourId, findColour (backgroundColourId)); | |||
tree.setRootItemVisible (false); | |||
tree.setDefaultOpenness (true); | |||
tree.setRootItem (treeItem); | |||
tree.setIndentSize (12); | |||
} | |||
KeyMappingEditorComponent::~KeyMappingEditorComponent() | |||
{ | |||
tree.setRootItem (nullptr); | |||
} | |||
//============================================================================== | |||
void KeyMappingEditorComponent::setColours (Colour mainBackground, | |||
Colour textColour) | |||
{ | |||
setColour (backgroundColourId, mainBackground); | |||
setColour (textColourId, textColour); | |||
tree.setColour (TreeView::backgroundColourId, mainBackground); | |||
} | |||
void KeyMappingEditorComponent::parentHierarchyChanged() | |||
{ | |||
treeItem->changeListenerCallback (nullptr); | |||
} | |||
void KeyMappingEditorComponent::resized() | |||
{ | |||
int h = getHeight(); | |||
if (resetButton.isVisible()) | |||
{ | |||
const int buttonHeight = 20; | |||
h -= buttonHeight + 8; | |||
int x = getWidth() - 8; | |||
resetButton.changeWidthToFitText (buttonHeight); | |||
resetButton.setTopRightPosition (x, h + 6); | |||
} | |||
tree.setBounds (0, 0, getWidth(), h); | |||
} | |||
//============================================================================== | |||
bool KeyMappingEditorComponent::shouldCommandBeIncluded (const CommandID commandID) | |||
{ | |||
auto* ci = mappings.getCommandManager().getCommandForID (commandID); | |||
return ci != nullptr && (ci->flags & ApplicationCommandInfo::hiddenFromKeyEditor) == 0; | |||
} | |||
bool KeyMappingEditorComponent::isCommandReadOnly (const CommandID commandID) | |||
{ | |||
auto* ci = mappings.getCommandManager().getCommandForID (commandID); | |||
return ci != nullptr && (ci->flags & ApplicationCommandInfo::readOnlyInKeyEditor) != 0; | |||
} | |||
String KeyMappingEditorComponent::getDescriptionForKeyPress (const KeyPress& key) | |||
{ | |||
return key.getTextDescription(); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,134 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A component to allow editing of the keymaps stored by a KeyPressMappingSet | |||
object. | |||
@see KeyPressMappingSet | |||
*/ | |||
class JUCE_API KeyMappingEditorComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates a KeyMappingEditorComponent. | |||
@param mappingSet this is the set of mappings to display and edit. Make sure the | |||
mappings object is not deleted before this component! | |||
@param showResetToDefaultButton if true, then at the bottom of the list, the | |||
component will include a 'reset to defaults' button. | |||
*/ | |||
KeyMappingEditorComponent (KeyPressMappingSet& mappingSet, | |||
bool showResetToDefaultButton); | |||
/** Destructor. */ | |||
~KeyMappingEditorComponent(); | |||
//============================================================================== | |||
/** Sets up the colours to use for parts of the component. | |||
@param mainBackground colour to use for most of the background | |||
@param textColour colour to use for the text | |||
*/ | |||
void setColours (Colour mainBackground, | |||
Colour textColour); | |||
/** Returns the KeyPressMappingSet that this component is acting upon. */ | |||
KeyPressMappingSet& getMappings() const noexcept { return mappings; } | |||
/** Returns the ApplicationCommandManager that this component is connected to. */ | |||
ApplicationCommandManager& getCommandManager() const noexcept { return mappings.getCommandManager(); } | |||
//============================================================================== | |||
/** Can be overridden if some commands need to be excluded from the list. | |||
By default this will use the KeyPressMappingSet's shouldCommandBeVisibleInEditor() | |||
method to decide what to return, but you can override it to handle special cases. | |||
*/ | |||
virtual bool shouldCommandBeIncluded (CommandID commandID); | |||
/** Can be overridden to indicate that some commands are shown as read-only. | |||
By default this will use the KeyPressMappingSet's shouldCommandBeReadOnlyInEditor() | |||
method to decide what to return, but you can override it to handle special cases. | |||
*/ | |||
virtual bool isCommandReadOnly (CommandID commandID); | |||
/** This can be overridden to let you change the format of the string used | |||
to describe a keypress. | |||
This is handy if you're using non-standard KeyPress objects, e.g. for custom | |||
keys that are triggered by something else externally. If you override the | |||
method, be sure to let the base class's method handle keys you're not | |||
interested in. | |||
*/ | |||
virtual String getDescriptionForKeyPress (const KeyPress& key); | |||
//============================================================================== | |||
/** A set of colour IDs to use to change the colour of various aspects of the editor. | |||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() | |||
methods. | |||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour | |||
*/ | |||
enum ColourIds | |||
{ | |||
backgroundColourId = 0x100ad00, /**< The background colour to fill the editor background. */ | |||
textColourId = 0x100ad01, /**< The colour for the text. */ | |||
}; | |||
//============================================================================== | |||
/** @internal */ | |||
void parentHierarchyChanged() override; | |||
/** @internal */ | |||
void resized() override; | |||
private: | |||
//============================================================================== | |||
KeyPressMappingSet& mappings; | |||
TreeView tree; | |||
TextButton resetButton; | |||
class TopLevelItem; | |||
class ChangeKeyButton; | |||
class MappingItem; | |||
class CategoryItem; | |||
class ItemComponent; | |||
friend class TopLevelItem; | |||
friend struct ContainerDeletePolicy<ChangeKeyButton>; | |||
friend struct ContainerDeletePolicy<TopLevelItem>; | |||
ScopedPointer<TopLevelItem> treeItem; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (KeyMappingEditorComponent) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,507 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_ENABLE_LIVE_CONSTANT_EDITOR | |||
namespace LiveConstantEditor | |||
{ | |||
//============================================================================== | |||
class AllComponentRepainter : private Timer, | |||
private DeletedAtShutdown | |||
{ | |||
public: | |||
AllComponentRepainter() {} | |||
~AllComponentRepainter() { clearSingletonInstance(); } | |||
juce_DeclareSingleton (AllComponentRepainter, false) | |||
void trigger() | |||
{ | |||
if (! isTimerRunning()) | |||
startTimer (100); | |||
} | |||
private: | |||
void timerCallback() override | |||
{ | |||
stopTimer(); | |||
Array<Component*> alreadyDone; | |||
for (int i = TopLevelWindow::getNumTopLevelWindows(); --i >= 0;) | |||
if (auto* c = TopLevelWindow::getTopLevelWindow(i)) | |||
repaintAndResizeAllComps (c, alreadyDone); | |||
auto& desktop = Desktop::getInstance(); | |||
for (int i = desktop.getNumComponents(); --i >= 0;) | |||
if (auto* c = desktop.getComponent(i)) | |||
repaintAndResizeAllComps (c, alreadyDone); | |||
} | |||
static void repaintAndResizeAllComps (Component::SafePointer<Component> c, | |||
Array<Component*>& alreadyDone) | |||
{ | |||
if (c->isVisible() && ! alreadyDone.contains (c)) | |||
{ | |||
c->repaint(); | |||
c->resized(); | |||
for (int i = c->getNumChildComponents(); --i >= 0;) | |||
{ | |||
if (auto* child = c->getChildComponent(i)) | |||
{ | |||
repaintAndResizeAllComps (child, alreadyDone); | |||
alreadyDone.add (child); | |||
} | |||
if (c == nullptr) | |||
break; | |||
} | |||
} | |||
} | |||
}; | |||
juce_ImplementSingleton (AllComponentRepainter) | |||
juce_ImplementSingleton (ValueList) | |||
//============================================================================== | |||
int64 parseInt (String s) | |||
{ | |||
s = s.trimStart(); | |||
if (s.startsWithChar ('-')) | |||
return -parseInt (s.substring (1)); | |||
if (s.startsWith ("0x")) | |||
return s.substring(2).getHexValue64(); | |||
return s.getLargeIntValue(); | |||
} | |||
double parseDouble (const String& s) | |||
{ | |||
return s.retainCharacters ("0123456789.eE-").getDoubleValue(); | |||
} | |||
String intToString (int v, bool preferHex) { return preferHex ? "0x" + String::toHexString (v) : String (v); } | |||
String intToString (int64 v, bool preferHex) { return preferHex ? "0x" + String::toHexString (v) : String (v); } | |||
//============================================================================== | |||
LiveValueBase::LiveValueBase (const char* file, int line) | |||
: sourceFile (file), sourceLine (line) | |||
{ | |||
name = File (sourceFile).getFileName() + " : " + String (sourceLine); | |||
} | |||
LiveValueBase::~LiveValueBase() | |||
{ | |||
} | |||
//============================================================================== | |||
LivePropertyEditorBase::LivePropertyEditorBase (LiveValueBase& v, CodeDocument& d) | |||
: value (v), document (d), sourceEditor (document, &tokeniser) | |||
{ | |||
setSize (600, 100); | |||
addAndMakeVisible (name); | |||
addAndMakeVisible (resetButton); | |||
addAndMakeVisible (valueEditor); | |||
addAndMakeVisible (sourceEditor); | |||
findOriginalValueInCode(); | |||
selectOriginalValue(); | |||
name.setFont (13.0f); | |||
name.setText (v.name, dontSendNotification); | |||
valueEditor.setMultiLine (v.isString()); | |||
valueEditor.setReturnKeyStartsNewLine (v.isString()); | |||
valueEditor.setText (v.getStringValue (wasHex), dontSendNotification); | |||
valueEditor.addListener (this); | |||
sourceEditor.setReadOnly (true); | |||
sourceEditor.setFont (sourceEditor.getFont().withHeight (13.0f)); | |||
resetButton.addListener (this); | |||
} | |||
void LivePropertyEditorBase::paint (Graphics& g) | |||
{ | |||
g.setColour (Colours::white); | |||
g.fillRect (getLocalBounds().removeFromBottom (1)); | |||
} | |||
void LivePropertyEditorBase::resized() | |||
{ | |||
auto r = getLocalBounds().reduced (0, 3).withTrimmedBottom (1); | |||
auto left = r.removeFromLeft (jmax (200, r.getWidth() / 3)); | |||
auto top = left.removeFromTop (25); | |||
resetButton.setBounds (top.removeFromRight (35).reduced (0, 3)); | |||
name.setBounds (top); | |||
if (customComp != nullptr) | |||
{ | |||
valueEditor.setBounds (left.removeFromTop (25)); | |||
left.removeFromTop (2); | |||
customComp->setBounds (left); | |||
} | |||
else | |||
{ | |||
valueEditor.setBounds (left); | |||
} | |||
r.removeFromLeft (4); | |||
sourceEditor.setBounds (r); | |||
} | |||
void LivePropertyEditorBase::textEditorTextChanged (TextEditor&) | |||
{ | |||
applyNewValue (valueEditor.getText()); | |||
} | |||
void LivePropertyEditorBase::buttonClicked (Button*) | |||
{ | |||
applyNewValue (value.getOriginalStringValue (wasHex)); | |||
} | |||
void LivePropertyEditorBase::applyNewValue (const String& s) | |||
{ | |||
value.setStringValue (s); | |||
document.replaceSection (valueStart.getPosition(), valueEnd.getPosition(), value.getCodeValue (wasHex)); | |||
document.clearUndoHistory(); | |||
selectOriginalValue(); | |||
valueEditor.setText (s, dontSendNotification); | |||
AllComponentRepainter::getInstance()->trigger(); | |||
} | |||
void LivePropertyEditorBase::selectOriginalValue() | |||
{ | |||
sourceEditor.selectRegion (valueStart, valueEnd); | |||
} | |||
void LivePropertyEditorBase::findOriginalValueInCode() | |||
{ | |||
CodeDocument::Position pos (document, value.sourceLine, 0); | |||
auto line = pos.getLineText(); | |||
auto p = line.getCharPointer(); | |||
p = CharacterFunctions::find (p, CharPointer_ASCII ("JUCE_LIVE_CONSTANT")); | |||
if (p.isEmpty()) | |||
{ | |||
// Not sure how this would happen - some kind of mix-up between source code and line numbers.. | |||
jassertfalse; | |||
return; | |||
} | |||
p += (int) (sizeof ("JUCE_LIVE_CONSTANT") - 1); | |||
p = p.findEndOfWhitespace(); | |||
if (! CharacterFunctions::find (p, CharPointer_ASCII ("JUCE_LIVE_CONSTANT")).isEmpty()) | |||
{ | |||
// Aargh! You've added two JUCE_LIVE_CONSTANT macros on the same line! | |||
// They're identified by their line number, so you must make sure each | |||
// one goes on a separate line! | |||
jassertfalse; | |||
} | |||
if (p.getAndAdvance() == '(') | |||
{ | |||
auto start = p, end = p; | |||
int depth = 1; | |||
while (! end.isEmpty()) | |||
{ | |||
auto c = end.getAndAdvance(); | |||
if (c == '(') ++depth; | |||
if (c == ')') --depth; | |||
if (depth == 0) | |||
{ | |||
--end; | |||
break; | |||
} | |||
} | |||
if (end > start) | |||
{ | |||
valueStart = CodeDocument::Position (document, value.sourceLine, (int) (start - line.getCharPointer())); | |||
valueEnd = CodeDocument::Position (document, value.sourceLine, (int) (end - line.getCharPointer())); | |||
valueStart.setPositionMaintained (true); | |||
valueEnd.setPositionMaintained (true); | |||
wasHex = String (start, end).containsIgnoreCase ("0x"); | |||
} | |||
} | |||
} | |||
//============================================================================== | |||
class ValueListHolderComponent : public Component | |||
{ | |||
public: | |||
ValueListHolderComponent (ValueList& l) : valueList (l) | |||
{ | |||
setVisible (true); | |||
} | |||
void addItem (int width, LiveValueBase& v, CodeDocument& doc) | |||
{ | |||
addAndMakeVisible (editors.add (v.createPropertyComponent (doc))); | |||
layout (width); | |||
} | |||
void layout (int width) | |||
{ | |||
setSize (width, editors.size() * itemHeight); | |||
resized(); | |||
} | |||
void resized() override | |||
{ | |||
Rectangle<int> r (getLocalBounds().reduced (2, 0)); | |||
for (int i = 0; i < editors.size(); ++i) | |||
editors.getUnchecked(i)->setBounds (r.removeFromTop (itemHeight)); | |||
} | |||
enum { itemHeight = 120 }; | |||
ValueList& valueList; | |||
OwnedArray<LivePropertyEditorBase> editors; | |||
}; | |||
//============================================================================== | |||
class ValueList::EditorWindow : public DocumentWindow, | |||
private DeletedAtShutdown | |||
{ | |||
public: | |||
EditorWindow (ValueList& list) | |||
: DocumentWindow ("Live Values", Colours::lightgrey, DocumentWindow::closeButton) | |||
{ | |||
setLookAndFeel (&lookAndFeel); | |||
setUsingNativeTitleBar (true); | |||
viewport.setViewedComponent (new ValueListHolderComponent (list), true); | |||
viewport.setSize (700, 600); | |||
viewport.setScrollBarsShown (true, false); | |||
setContentNonOwned (&viewport, true); | |||
setResizable (true, false); | |||
setResizeLimits (500, 400, 10000, 10000); | |||
centreWithSize (getWidth(), getHeight()); | |||
setVisible (true); | |||
} | |||
void closeButtonPressed() override | |||
{ | |||
setVisible (false); | |||
} | |||
void updateItems (ValueList& list) | |||
{ | |||
if (auto* l = dynamic_cast<ValueListHolderComponent*> (viewport.getViewedComponent())) | |||
{ | |||
while (l->getNumChildComponents() < list.values.size()) | |||
{ | |||
if (auto* v = list.values [l->getNumChildComponents()]) | |||
l->addItem (viewport.getMaximumVisibleWidth(), *v, list.getDocument (v->sourceFile)); | |||
else | |||
break; | |||
} | |||
setVisible (true); | |||
} | |||
} | |||
void resized() override | |||
{ | |||
DocumentWindow::resized(); | |||
if (auto* l = dynamic_cast<ValueListHolderComponent*> (viewport.getViewedComponent())) | |||
l->layout (viewport.getMaximumVisibleWidth()); | |||
} | |||
Viewport viewport; | |||
LookAndFeel_V3 lookAndFeel; | |||
}; | |||
//============================================================================== | |||
ValueList::ValueList() {} | |||
ValueList::~ValueList() { clearSingletonInstance(); } | |||
void ValueList::addValue (LiveValueBase* v) | |||
{ | |||
values.add (v); | |||
triggerAsyncUpdate(); | |||
} | |||
void ValueList::handleAsyncUpdate() | |||
{ | |||
if (editorWindow == nullptr) | |||
editorWindow = new EditorWindow (*this); | |||
editorWindow->updateItems (*this); | |||
} | |||
CodeDocument& ValueList::getDocument (const File& file) | |||
{ | |||
const int index = documentFiles.indexOf (file.getFullPathName()); | |||
if (index >= 0) | |||
return *documents.getUnchecked (index); | |||
auto* doc = documents.add (new CodeDocument()); | |||
documentFiles.add (file); | |||
doc->replaceAllContent (file.loadFileAsString()); | |||
doc->clearUndoHistory(); | |||
return *doc; | |||
} | |||
//============================================================================== | |||
struct ColourEditorComp : public Component, | |||
private ChangeListener | |||
{ | |||
ColourEditorComp (LivePropertyEditorBase& e) : editor (e) | |||
{ | |||
setMouseCursor (MouseCursor::PointingHandCursor); | |||
} | |||
Colour getColour() const | |||
{ | |||
return Colour ((uint32) parseInt (editor.value.getStringValue (false))); | |||
} | |||
void paint (Graphics& g) override | |||
{ | |||
g.fillCheckerBoard (getLocalBounds(), 6, 6, | |||
Colour (0xffdddddd).overlaidWith (getColour()), | |||
Colour (0xffffffff).overlaidWith (getColour())); | |||
} | |||
void mouseDown (const MouseEvent&) override | |||
{ | |||
auto* colourSelector = new ColourSelector(); | |||
colourSelector->setName ("Colour"); | |||
colourSelector->setCurrentColour (getColour()); | |||
colourSelector->addChangeListener (this); | |||
colourSelector->setColour (ColourSelector::backgroundColourId, Colours::transparentBlack); | |||
colourSelector->setSize (300, 400); | |||
CallOutBox::launchAsynchronously (colourSelector, getScreenBounds(), nullptr); | |||
} | |||
void changeListenerCallback (ChangeBroadcaster* source) override | |||
{ | |||
if (auto* cs = dynamic_cast<ColourSelector*> (source)) | |||
editor.applyNewValue (getAsString (cs->getCurrentColour(), true)); | |||
repaint(); | |||
} | |||
LivePropertyEditorBase& editor; | |||
}; | |||
Component* createColourEditor (LivePropertyEditorBase& editor) | |||
{ | |||
return new ColourEditorComp (editor); | |||
} | |||
//============================================================================== | |||
struct SliderComp : public Component, | |||
private Slider::Listener | |||
{ | |||
SliderComp (LivePropertyEditorBase& e, bool useFloat) | |||
: editor (e), isFloat (useFloat) | |||
{ | |||
slider.setTextBoxStyle (Slider::NoTextBox, true, 0, 0); | |||
addAndMakeVisible (slider); | |||
updateRange(); | |||
slider.addListener (this); | |||
} | |||
virtual void updateRange() | |||
{ | |||
double v = isFloat ? parseDouble (editor.value.getStringValue (false)) | |||
: (double) parseInt (editor.value.getStringValue (false)); | |||
double range = isFloat ? 10 : 100; | |||
slider.setRange (v - range, v + range); | |||
slider.setValue (v, dontSendNotification); | |||
} | |||
void sliderValueChanged (Slider*) override | |||
{ | |||
editor.applyNewValue (isFloat ? getAsString ((double) slider.getValue(), editor.wasHex) | |||
: getAsString ((int64) slider.getValue(), editor.wasHex)); | |||
} | |||
void sliderDragStarted (Slider*) override {} | |||
void sliderDragEnded (Slider*) override { updateRange(); } | |||
void resized() override | |||
{ | |||
slider.setBounds (getLocalBounds().removeFromTop (25)); | |||
} | |||
LivePropertyEditorBase& editor; | |||
Slider slider; | |||
bool isFloat; | |||
}; | |||
//============================================================================== | |||
struct BoolSliderComp : public SliderComp | |||
{ | |||
BoolSliderComp (LivePropertyEditorBase& e) : SliderComp (e, false) {} | |||
void updateRange() override | |||
{ | |||
slider.setRange (0.0, 1.0, dontSendNotification); | |||
slider.setValue (editor.value.getStringValue (false) == "true", dontSendNotification); | |||
} | |||
void sliderValueChanged (Slider*) override { editor.applyNewValue (slider.getValue() > 0.5 ? "true" : "false"); } | |||
}; | |||
Component* createIntegerSlider (LivePropertyEditorBase& editor) { return new SliderComp (editor, false); } | |||
Component* createFloatSlider (LivePropertyEditorBase& editor) { return new SliderComp (editor, true); } | |||
Component* createBoolSlider (LivePropertyEditorBase& editor) { return new BoolSliderComp (editor); } | |||
} | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,314 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_ENABLE_LIVE_CONSTANT_EDITOR && ! DOXYGEN | |||
//============================================================================== | |||
/** You can safely ignore all the stuff in this namespace - it's a bunch of boilerplate | |||
code used to implement the JUCE_LIVE_CONSTANT functionality. | |||
*/ | |||
namespace LiveConstantEditor | |||
{ | |||
int64 parseInt (String); | |||
double parseDouble (const String&); | |||
String intToString (int, bool preferHex); | |||
String intToString (int64, bool preferHex); | |||
template <typename Type> | |||
static void setFromString (Type& v, const String& s) { v = static_cast<Type> (s); } | |||
inline void setFromString (char& v, const String& s) { v = (char) parseInt (s); } | |||
inline void setFromString (unsigned char& v, const String& s) { v = (unsigned char) parseInt (s); } | |||
inline void setFromString (short& v, const String& s) { v = (short) parseInt (s); } | |||
inline void setFromString (unsigned short& v, const String& s) { v = (unsigned short) parseInt (s); } | |||
inline void setFromString (int& v, const String& s) { v = (int) parseInt (s); } | |||
inline void setFromString (unsigned int& v, const String& s) { v = (unsigned int) parseInt (s); } | |||
inline void setFromString (long& v, const String& s) { v = (long) parseInt (s); } | |||
inline void setFromString (unsigned long& v, const String& s) { v = (unsigned long) parseInt (s); } | |||
inline void setFromString (int64& v, const String& s) { v = (int64) parseInt (s); } | |||
inline void setFromString (uint64& v, const String& s) { v = (uint64) parseInt (s); } | |||
inline void setFromString (double& v, const String& s) { v = parseDouble (s); } | |||
inline void setFromString (float& v, const String& s) { v = (float) parseDouble (s); } | |||
inline void setFromString (bool& v, const String& s) { v = (s == "true"); } | |||
inline void setFromString (String& v, const String& s) { v = s; } | |||
inline void setFromString (Colour& v, const String& s) { v = Colour ((uint32) parseInt (s)); } | |||
template <typename Type> | |||
inline String getAsString (const Type& v, bool) { return String (v); } | |||
inline String getAsString (char v, bool preferHex) { return intToString ((int) v, preferHex); } | |||
inline String getAsString (unsigned char v, bool preferHex) { return intToString ((int) v, preferHex); } | |||
inline String getAsString (short v, bool preferHex) { return intToString ((int) v, preferHex); } | |||
inline String getAsString (unsigned short v, bool preferHex) { return intToString ((int) v, preferHex); } | |||
inline String getAsString (int v, bool preferHex) { return intToString ((int) v, preferHex); } | |||
inline String getAsString (unsigned int v, bool preferHex) { return intToString ((int) v, preferHex); } | |||
inline String getAsString (bool v, bool) { return v ? "true" : "false"; } | |||
inline String getAsString (int64 v, bool preferHex) { return intToString ((int64) v, preferHex); } | |||
inline String getAsString (uint64 v, bool preferHex) { return intToString ((int64) v, preferHex); } | |||
inline String getAsString (Colour v, bool) { return intToString ((int) v.getARGB(), true); } | |||
template <typename Type> struct isStringType { enum { value = 0 }; }; | |||
template <> struct isStringType<String> { enum { value = 1 }; }; | |||
template <typename Type> | |||
inline String getAsCode (Type& v, bool preferHex) { return getAsString (v, preferHex); } | |||
inline String getAsCode (Colour v, bool) { return "Colour (0x" + String::toHexString ((int) v.getARGB()).paddedLeft ('0', 8) + ")"; } | |||
inline String getAsCode (const String& v, bool) { return CppTokeniserFunctions::addEscapeChars(v).quoted(); } | |||
inline String getAsCode (const char* v, bool) { return getAsCode (String (v), false); } | |||
template <typename Type> | |||
inline const char* castToCharPointer (const Type&) { return ""; } | |||
inline const char* castToCharPointer (const String& s) { return s.toRawUTF8(); } | |||
struct LivePropertyEditorBase; | |||
//============================================================================== | |||
struct JUCE_API LiveValueBase | |||
{ | |||
LiveValueBase (const char* file, int line); | |||
virtual ~LiveValueBase(); | |||
virtual LivePropertyEditorBase* createPropertyComponent (CodeDocument&) = 0; | |||
virtual String getStringValue (bool preferHex) const = 0; | |||
virtual String getCodeValue (bool preferHex) const = 0; | |||
virtual void setStringValue (const String&) = 0; | |||
virtual String getOriginalStringValue (bool preferHex) const = 0; | |||
virtual bool isString() const = 0; | |||
String name, sourceFile; | |||
int sourceLine; | |||
JUCE_DECLARE_NON_COPYABLE (LiveValueBase) | |||
}; | |||
//============================================================================== | |||
struct JUCE_API LivePropertyEditorBase : public Component, | |||
private TextEditor::Listener, | |||
private Button::Listener | |||
{ | |||
LivePropertyEditorBase (LiveValueBase&, CodeDocument&); | |||
void paint (Graphics&) override; | |||
void resized() override; | |||
void textEditorTextChanged (TextEditor&) override; | |||
void buttonClicked (Button*) override; | |||
void applyNewValue (const String&); | |||
void selectOriginalValue(); | |||
void findOriginalValueInCode(); | |||
LiveValueBase& value; | |||
Label name; | |||
TextEditor valueEditor; | |||
TextButton resetButton { "reset" }; | |||
CodeDocument& document; | |||
CPlusPlusCodeTokeniser tokeniser; | |||
CodeEditorComponent sourceEditor; | |||
CodeDocument::Position valueStart, valueEnd; | |||
ScopedPointer<Component> customComp; | |||
bool wasHex = false; | |||
JUCE_DECLARE_NON_COPYABLE (LivePropertyEditorBase) | |||
}; | |||
//============================================================================== | |||
Component* createColourEditor (LivePropertyEditorBase&); | |||
Component* createIntegerSlider (LivePropertyEditorBase&); | |||
Component* createFloatSlider (LivePropertyEditorBase&); | |||
Component* createBoolSlider (LivePropertyEditorBase&); | |||
template <typename Type> struct CustomEditor { static Component* create (LivePropertyEditorBase&) { return nullptr; } }; | |||
template<> struct CustomEditor<char> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<unsigned char> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<int> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<unsigned int> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<short> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<unsigned short> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<int64> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<uint64> { static Component* create (LivePropertyEditorBase& e) { return createIntegerSlider (e); } }; | |||
template<> struct CustomEditor<float> { static Component* create (LivePropertyEditorBase& e) { return createFloatSlider (e); } }; | |||
template<> struct CustomEditor<double> { static Component* create (LivePropertyEditorBase& e) { return createFloatSlider (e); } }; | |||
template<> struct CustomEditor<Colour> { static Component* create (LivePropertyEditorBase& e) { return createColourEditor (e); } }; | |||
template<> struct CustomEditor<bool> { static Component* create (LivePropertyEditorBase& e) { return createBoolSlider (e); } }; | |||
template <typename Type> | |||
struct LivePropertyEditor : public LivePropertyEditorBase | |||
{ | |||
template <typename ValueType> | |||
LivePropertyEditor (ValueType& v, CodeDocument& d) : LivePropertyEditorBase (v, d) | |||
{ | |||
addAndMakeVisible (customComp = CustomEditor<Type>::create (*this)); | |||
} | |||
}; | |||
//============================================================================== | |||
template <typename Type> | |||
struct LiveValue : public LiveValueBase | |||
{ | |||
LiveValue (const char* file, int line, const Type& initialValue) | |||
: LiveValueBase (file, line), value (initialValue), originalValue (initialValue) | |||
{} | |||
operator Type() const noexcept { return value; } | |||
operator const char*() const { return castToCharPointer (value); } | |||
LivePropertyEditorBase* createPropertyComponent (CodeDocument& doc) override | |||
{ | |||
return new LivePropertyEditor<Type> (*this, doc); | |||
} | |||
String getStringValue (bool preferHex) const override { return getAsString (value, preferHex); } | |||
String getCodeValue (bool preferHex) const override { return getAsCode (value, preferHex); } | |||
String getOriginalStringValue (bool preferHex) const override { return getAsString (originalValue, preferHex); } | |||
void setStringValue (const String& s) override { setFromString (value, s); } | |||
bool isString() const override { return isStringType<Type>::value; } | |||
Type value, originalValue; | |||
JUCE_DECLARE_NON_COPYABLE (LiveValue) | |||
}; | |||
//============================================================================== | |||
class JUCE_API ValueList : private AsyncUpdater, | |||
private DeletedAtShutdown | |||
{ | |||
public: | |||
ValueList(); | |||
~ValueList(); | |||
juce_DeclareSingleton (ValueList, false) | |||
template <typename Type> | |||
LiveValue<Type>& getValue (const char* file, int line, const Type& initialValue) | |||
{ | |||
const ScopedLock sl (lock); | |||
typedef LiveValue<Type> ValueType; | |||
for (int i = 0; i < values.size(); ++i) | |||
{ | |||
LiveValueBase* v = values.getUnchecked(i); | |||
if (v->sourceLine == line && v->sourceFile == file) | |||
return *static_cast<ValueType*> (v); | |||
} | |||
ValueType* v = new ValueType (file, line, initialValue); | |||
addValue (v); | |||
return *v; | |||
} | |||
private: | |||
OwnedArray<LiveValueBase> values; | |||
OwnedArray<CodeDocument> documents; | |||
Array<File> documentFiles; | |||
class EditorWindow; | |||
friend class EditorWindow; | |||
friend struct ContainerDeletePolicy<EditorWindow>; | |||
Component::SafePointer<EditorWindow> editorWindow; | |||
CriticalSection lock; | |||
CodeDocument& getDocument (const File&); | |||
void addValue (LiveValueBase*); | |||
void handleAsyncUpdate() override; | |||
}; | |||
template <typename Type> | |||
inline LiveValue<Type>& getValue (const char* file, int line, const Type& initialValue) | |||
{ | |||
// If you hit this assertion then the __FILE__ macro is providing a | |||
// relative path instead of an absolute path. On Windows this will be | |||
// a path relative to the build directory rather than the currently | |||
// running application. To fix this you must compile with the /FC flag. | |||
jassert (File::isAbsolutePath (file)); | |||
return ValueList::getInstance()->getValue (file, line, initialValue); | |||
} | |||
inline LiveValue<String>& getValue (const char* file, int line, const char* initialValue) | |||
{ | |||
return getValue (file, line, String (initialValue)); | |||
} | |||
} | |||
#endif | |||
//============================================================================== | |||
#if JUCE_ENABLE_LIVE_CONSTANT_EDITOR || DOXYGEN | |||
/** | |||
This macro wraps a primitive constant value in some cunning boilerplate code that allows | |||
its value to be interactively tweaked in a popup window while your application is running. | |||
In a release build, this macro disappears and is replaced by only the constant that it | |||
wraps, but if JUCE_ENABLE_LIVE_CONSTANT_EDITOR is enabled, it injects a class wrapper | |||
that automatically pops-up a window containing an editor that allows the value to be | |||
tweaked at run-time. The editor window will also force all visible components to be | |||
resized and repainted whenever a value is changed, so that if you use this to wrap | |||
a colour or layout parameter, you'll be able to immediately see the effects of changing it. | |||
The editor will also load the original source-file that contains each JUCE_LIVE_CONSTANT | |||
macro, and will display a preview of the modified source code as you adjust the values. | |||
Things to note: | |||
- Only one of these per line! The __FILE__ and __LINE__ macros are used to identify | |||
the value, so things will get confused if you have more than one per line | |||
- Obviously because it needs to load the source code based on the __FILE__ macro, | |||
it'll only work if the source files are stored locally in the same location as they | |||
were when you compiled the program. | |||
- It's only designed to cope with simple types: primitives, string literals, and | |||
the Colour class, so if you try using it for other classes or complex expressions, | |||
good luck! | |||
- The editor window will get popped up whenever a new value is used for the first | |||
time. You can close the window, but there's no way to get it back without restarting | |||
the app! | |||
e.g. in this example the colours, font size, and text used in the paint method can | |||
all be adjusted live: | |||
@code | |||
void MyComp::paint (Graphics& g) override | |||
{ | |||
g.fillAll (JUCE_LIVE_CONSTANT (Colour (0xffddddff))); | |||
Colour fontColour = JUCE_LIVE_CONSTANT (Colour (0xff005500)); | |||
float fontSize = JUCE_LIVE_CONSTANT (16.0f); | |||
g.setColour (fontColour); | |||
g.setFont (fontSize); | |||
g.drawFittedText (JUCE_LIVE_CONSTANT ("Hello world!"), | |||
getLocalBounds(), Justification::centred, 2); | |||
} | |||
@endcode | |||
*/ | |||
#define JUCE_LIVE_CONSTANT(initialValue) \ | |||
(juce::LiveConstantEditor::getValue (__FILE__, __LINE__ - 1, initialValue)) | |||
#else | |||
#define JUCE_LIVE_CONSTANT(initialValue) \ | |||
(initialValue) | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,157 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
PreferencesPanel::PreferencesPanel() | |||
: buttonSize (70) | |||
{ | |||
} | |||
PreferencesPanel::~PreferencesPanel() | |||
{ | |||
} | |||
int PreferencesPanel::getButtonSize() const noexcept | |||
{ | |||
return buttonSize; | |||
} | |||
void PreferencesPanel::setButtonSize (int newSize) | |||
{ | |||
buttonSize = newSize; | |||
resized(); | |||
} | |||
//============================================================================== | |||
void PreferencesPanel::addSettingsPage (const String& title, | |||
const Drawable* icon, | |||
const Drawable* overIcon, | |||
const Drawable* downIcon) | |||
{ | |||
DrawableButton* const button = new DrawableButton (title, DrawableButton::ImageAboveTextLabel); | |||
buttons.add (button); | |||
button->setImages (icon, overIcon, downIcon); | |||
button->setRadioGroupId (1); | |||
button->addListener (this); | |||
button->setClickingTogglesState (true); | |||
button->setWantsKeyboardFocus (false); | |||
addAndMakeVisible (button); | |||
resized(); | |||
if (currentPage == nullptr) | |||
setCurrentPage (title); | |||
} | |||
void PreferencesPanel::addSettingsPage (const String& title, const void* imageData, const int imageDataSize) | |||
{ | |||
DrawableImage icon, iconOver, iconDown; | |||
icon.setImage (ImageCache::getFromMemory (imageData, imageDataSize)); | |||
iconOver.setImage (ImageCache::getFromMemory (imageData, imageDataSize)); | |||
iconOver.setOverlayColour (Colours::black.withAlpha (0.12f)); | |||
iconDown.setImage (ImageCache::getFromMemory (imageData, imageDataSize)); | |||
iconDown.setOverlayColour (Colours::black.withAlpha (0.25f)); | |||
addSettingsPage (title, &icon, &iconOver, &iconDown); | |||
} | |||
//============================================================================== | |||
void PreferencesPanel::showInDialogBox (const String& dialogTitle, int dialogWidth, int dialogHeight, Colour backgroundColour) | |||
{ | |||
setSize (dialogWidth, dialogHeight); | |||
DialogWindow::LaunchOptions o; | |||
o.content.setNonOwned (this); | |||
o.dialogTitle = dialogTitle; | |||
o.dialogBackgroundColour = backgroundColour; | |||
o.escapeKeyTriggersCloseButton = false; | |||
o.useNativeTitleBar = false; | |||
o.resizable = false; | |||
o.launchAsync(); | |||
} | |||
//============================================================================== | |||
void PreferencesPanel::resized() | |||
{ | |||
for (int i = 0; i < buttons.size(); ++i) | |||
buttons.getUnchecked(i)->setBounds (i * buttonSize, 0, buttonSize, buttonSize); | |||
if (currentPage != nullptr) | |||
currentPage->setBounds (getLocalBounds().withTop (buttonSize + 5)); | |||
} | |||
void PreferencesPanel::paint (Graphics& g) | |||
{ | |||
g.setColour (Colours::grey); | |||
g.fillRect (0, buttonSize + 2, getWidth(), 1); | |||
} | |||
void PreferencesPanel::setCurrentPage (const String& pageName) | |||
{ | |||
if (currentPageName != pageName) | |||
{ | |||
currentPageName = pageName; | |||
currentPage = nullptr; | |||
currentPage = createComponentForPage (pageName); | |||
if (currentPage != nullptr) | |||
{ | |||
addAndMakeVisible (currentPage); | |||
currentPage->toBack(); | |||
resized(); | |||
} | |||
for (int i = 0; i < buttons.size(); ++i) | |||
{ | |||
if (buttons.getUnchecked(i)->getName() == pageName) | |||
{ | |||
buttons.getUnchecked(i)->setToggleState (true, dontSendNotification); | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
void PreferencesPanel::buttonClicked (Button*) | |||
{ | |||
for (int i = 0; i < buttons.size(); ++i) | |||
{ | |||
if (buttons.getUnchecked(i)->getToggleState()) | |||
{ | |||
setCurrentPage (buttons.getUnchecked(i)->getName()); | |||
break; | |||
} | |||
} | |||
} | |||
} // namespace juce |
@@ -0,0 +1,147 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
A component with a set of buttons at the top for changing between pages of | |||
preferences. | |||
This is just a handy way of writing a Mac-style preferences panel where you | |||
have a row of buttons along the top for the different preference categories, | |||
each button having an icon above its name. Clicking these will show an | |||
appropriate prefs page below it. | |||
You can either put one of these inside your own component, or just use the | |||
showInDialogBox() method to show it in a window and run it modally. | |||
To use it, just add a set of named pages with the addSettingsPage() method, | |||
and implement the createComponentForPage() method to create suitable components | |||
for each of these pages. | |||
*/ | |||
class JUCE_API PreferencesPanel : public Component, | |||
private Button::Listener | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates an empty panel. | |||
Use addSettingsPage() to add some pages to it in your constructor. | |||
*/ | |||
PreferencesPanel(); | |||
/** Destructor. */ | |||
~PreferencesPanel(); | |||
//============================================================================== | |||
/** Creates a page using a set of drawables to define the page's icon. | |||
Note that the other version of this method is much easier if you're using | |||
an image instead of a custom drawable. | |||
@param pageTitle the name of this preferences page - you'll need to | |||
make sure your createComponentForPage() method creates | |||
a suitable component when it is passed this name | |||
@param normalIcon the drawable to display in the page's button normally | |||
@param overIcon the drawable to display in the page's button when the mouse is over | |||
@param downIcon the drawable to display in the page's button when the button is down | |||
@see DrawableButton | |||
*/ | |||
void addSettingsPage (const String& pageTitle, | |||
const Drawable* normalIcon, | |||
const Drawable* overIcon, | |||
const Drawable* downIcon); | |||
/** Creates a page using a set of drawables to define the page's icon. | |||
The other version of this method gives you more control over the icon, but this | |||
one is much easier if you're just loading it from a file. | |||
@param pageTitle the name of this preferences page - you'll need to | |||
make sure your createComponentForPage() method creates | |||
a suitable component when it is passed this name | |||
@param imageData a block of data containing an image file, e.g. a jpeg, png or gif. | |||
For this to look good, you'll probably want to use a nice | |||
transparent png file. | |||
@param imageDataSize the size of the image data, in bytes | |||
*/ | |||
void addSettingsPage (const String& pageTitle, | |||
const void* imageData, | |||
int imageDataSize); | |||
/** Utility method to display this panel in a DialogWindow. | |||
Calling this will create a DialogWindow containing this panel with the | |||
given size and title, and will run it modally, returning when the user | |||
closes the dialog box. | |||
*/ | |||
void showInDialogBox (const String& dialogTitle, | |||
int dialogWidth, | |||
int dialogHeight, | |||
Colour backgroundColour = Colours::white); | |||
//============================================================================== | |||
/** Subclasses must override this to return a component for each preferences page. | |||
The subclass should return a pointer to a new component representing the named | |||
page, which the panel will then display. | |||
The panel will delete the component later when the user goes to another page | |||
or deletes the panel. | |||
*/ | |||
virtual Component* createComponentForPage (const String& pageName) = 0; | |||
//============================================================================== | |||
/** Changes the current page being displayed. */ | |||
void setCurrentPage (const String& pageName); | |||
/** Returns the size of the buttons shown along the top. */ | |||
int getButtonSize() const noexcept; | |||
/** Changes the size of the buttons shown along the top. */ | |||
void setButtonSize (int newSize); | |||
//============================================================================== | |||
/** @internal */ | |||
void resized() override; | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
void buttonClicked (Button*) override; | |||
private: | |||
//============================================================================== | |||
String currentPageName; | |||
ScopedPointer<Component> currentPage; | |||
OwnedArray<DrawableButton> buttons; | |||
int buttonSize; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PreferencesPanel) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,152 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
RecentlyOpenedFilesList::RecentlyOpenedFilesList() | |||
: maxNumberOfItems (10) | |||
{ | |||
} | |||
RecentlyOpenedFilesList::~RecentlyOpenedFilesList() | |||
{ | |||
} | |||
//============================================================================== | |||
void RecentlyOpenedFilesList::setMaxNumberOfItems (const int newMaxNumber) | |||
{ | |||
maxNumberOfItems = jmax (1, newMaxNumber); | |||
files.removeRange (maxNumberOfItems, getNumFiles()); | |||
} | |||
int RecentlyOpenedFilesList::getNumFiles() const | |||
{ | |||
return files.size(); | |||
} | |||
File RecentlyOpenedFilesList::getFile (const int index) const | |||
{ | |||
return File (files [index]); | |||
} | |||
void RecentlyOpenedFilesList::clear() | |||
{ | |||
files.clear(); | |||
} | |||
void RecentlyOpenedFilesList::addFile (const File& file) | |||
{ | |||
removeFile (file); | |||
files.insert (0, file.getFullPathName()); | |||
setMaxNumberOfItems (maxNumberOfItems); | |||
} | |||
void RecentlyOpenedFilesList::removeFile (const File& file) | |||
{ | |||
files.removeString (file.getFullPathName()); | |||
} | |||
void RecentlyOpenedFilesList::removeNonExistentFiles() | |||
{ | |||
for (int i = getNumFiles(); --i >= 0;) | |||
if (! getFile(i).exists()) | |||
files.remove (i); | |||
} | |||
//============================================================================== | |||
int RecentlyOpenedFilesList::createPopupMenuItems (PopupMenu& menuToAddTo, | |||
const int baseItemId, | |||
const bool showFullPaths, | |||
const bool dontAddNonExistentFiles, | |||
const File** filesToAvoid) | |||
{ | |||
int num = 0; | |||
for (int i = 0; i < getNumFiles(); ++i) | |||
{ | |||
const File f (getFile(i)); | |||
if ((! dontAddNonExistentFiles) || f.exists()) | |||
{ | |||
bool needsAvoiding = false; | |||
if (filesToAvoid != nullptr) | |||
{ | |||
for (const File** avoid = filesToAvoid; *avoid != nullptr; ++avoid) | |||
{ | |||
if (f == **avoid) | |||
{ | |||
needsAvoiding = true; | |||
break; | |||
} | |||
} | |||
} | |||
if (! needsAvoiding) | |||
{ | |||
menuToAddTo.addItem (baseItemId + i, | |||
showFullPaths ? f.getFullPathName() | |||
: f.getFileName()); | |||
++num; | |||
} | |||
} | |||
} | |||
return num; | |||
} | |||
//============================================================================== | |||
String RecentlyOpenedFilesList::toString() const | |||
{ | |||
return files.joinIntoString ("\n"); | |||
} | |||
void RecentlyOpenedFilesList::restoreFromString (const String& stringifiedVersion) | |||
{ | |||
clear(); | |||
files.addLines (stringifiedVersion); | |||
setMaxNumberOfItems (maxNumberOfItems); | |||
} | |||
//============================================================================== | |||
void RecentlyOpenedFilesList::registerRecentFileNatively (const File& file) | |||
{ | |||
#if JUCE_MAC | |||
JUCE_AUTORELEASEPOOL | |||
{ | |||
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: createNSURLFromFile (file)]; | |||
} | |||
#else | |||
ignoreUnused (file); | |||
#endif | |||
} | |||
} // namespace juce |
@@ -0,0 +1,164 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
Manages a set of files for use as a list of recently-opened documents. | |||
This is a handy class for holding your list of recently-opened documents, with | |||
helpful methods for things like purging any non-existent files, automatically | |||
adding them to a menu, and making persistence easy. | |||
@see File, FileBasedDocument | |||
*/ | |||
class JUCE_API RecentlyOpenedFilesList | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates an empty list. | |||
*/ | |||
RecentlyOpenedFilesList(); | |||
/** Destructor. */ | |||
~RecentlyOpenedFilesList(); | |||
//============================================================================== | |||
/** Sets a limit for the number of files that will be stored in the list. | |||
When addFile() is called, then if there is no more space in the list, the | |||
least-recently added file will be dropped. | |||
@see getMaxNumberOfItems | |||
*/ | |||
void setMaxNumberOfItems (int newMaxNumber); | |||
/** Returns the number of items that this list will store. | |||
@see setMaxNumberOfItems | |||
*/ | |||
int getMaxNumberOfItems() const noexcept { return maxNumberOfItems; } | |||
/** Returns the number of files in the list. | |||
The most recently added file is always at index 0. | |||
*/ | |||
int getNumFiles() const; | |||
/** Returns one of the files in the list. | |||
The most recently added file is always at index 0. | |||
*/ | |||
File getFile (int index) const; | |||
/** Returns an array of all the absolute pathnames in the list. | |||
*/ | |||
const StringArray& getAllFilenames() const noexcept { return files; } | |||
/** Clears all the files from the list. */ | |||
void clear(); | |||
/** Adds a file to the list. | |||
The file will be added at index 0. If this file is already in the list, it will | |||
be moved up to index 0, but a file can only appear once in the list. | |||
If the list already contains the maximum number of items that is permitted, the | |||
least-recently added file will be dropped from the end. | |||
*/ | |||
void addFile (const File& file); | |||
/** Removes a file from the list. */ | |||
void removeFile (const File& file); | |||
/** Checks each of the files in the list, removing any that don't exist. | |||
You might want to call this after reloading a list of files, or before putting them | |||
on a menu. | |||
*/ | |||
void removeNonExistentFiles(); | |||
/** Tells the OS to add a file to the OS-managed list of recent documents for this app. | |||
Not all OSes maintain a list of recent files for an application, so this | |||
function will have no effect on some OSes. Currently it's just implemented for OSX. | |||
*/ | |||
static void registerRecentFileNatively (const File& file); | |||
//============================================================================== | |||
/** Adds entries to a menu, representing each of the files in the list. | |||
This is handy for creating an "open recent file..." menu in your app. The | |||
menu items are numbered consecutively starting with the baseItemId value, | |||
and can either be added as complete pathnames, or just the last part of the | |||
filename. | |||
If dontAddNonExistentFiles is true, then each file will be checked and only those | |||
that exist will be added. | |||
If filesToAvoid is not a nullptr, then it is considered to be a zero-terminated array | |||
of pointers to file objects. Any files that appear in this list will not be added to | |||
the menu - the reason for this is that you might have a number of files already open, | |||
so might not want these to be shown in the menu. | |||
It returns the number of items that were added. | |||
*/ | |||
int createPopupMenuItems (PopupMenu& menuToAddItemsTo, | |||
int baseItemId, | |||
bool showFullPaths, | |||
bool dontAddNonExistentFiles, | |||
const File** filesToAvoid = nullptr); | |||
//============================================================================== | |||
/** Returns a string that encapsulates all the files in the list. | |||
The string that is returned can later be passed into restoreFromString() in | |||
order to recreate the list. This is handy for persisting your list, e.g. in | |||
a PropertiesFile object. | |||
@see restoreFromString | |||
*/ | |||
String toString() const; | |||
/** Restores the list from a previously stringified version of the list. | |||
Pass in a stringified version created with toString() in order to persist/restore | |||
your list. | |||
@see toString | |||
*/ | |||
void restoreFromString (const String& stringifiedVersion); | |||
private: | |||
//============================================================================== | |||
StringArray files; | |||
int maxNumberOfItems; | |||
JUCE_LEAK_DETECTOR (RecentlyOpenedFilesList) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,102 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
SplashScreen::SplashScreen (const String& title, const Image& image, bool useDropShadow) | |||
: Component (title), | |||
backgroundImage (image), | |||
clickCountToDelete (0) | |||
{ | |||
// You must supply a valid image here! | |||
jassert (backgroundImage.isValid()); | |||
setOpaque (! backgroundImage.hasAlphaChannel()); | |||
#if JUCE_IOS || JUCE_ANDROID | |||
const bool useFullScreen = true; | |||
#else | |||
const bool useFullScreen = false; | |||
#endif | |||
makeVisible (image.getWidth(), image.getHeight(), useDropShadow, useFullScreen); | |||
} | |||
SplashScreen::SplashScreen (const String& title, int width, int height, bool useDropShadow) | |||
: Component (title), | |||
clickCountToDelete (0) | |||
{ | |||
makeVisible (width, height, useDropShadow, false); | |||
} | |||
void SplashScreen::makeVisible (int w, int h, bool useDropShadow, bool fullscreen) | |||
{ | |||
clickCountToDelete = Desktop::getInstance().getMouseButtonClickCounter(); | |||
creationTime = Time::getCurrentTime(); | |||
const Rectangle<int> screenSize = Desktop::getInstance().getDisplays().getMainDisplay().userArea; | |||
const int width = (fullscreen ? screenSize.getWidth() : w); | |||
const int height = (fullscreen ? screenSize.getHeight() : h); | |||
setAlwaysOnTop (true); | |||
setVisible (true); | |||
centreWithSize (width, height); | |||
addToDesktop (useDropShadow ? ComponentPeer::windowHasDropShadow : 0); | |||
if (fullscreen) | |||
getPeer()->setFullScreen (true); | |||
toFront (false); | |||
} | |||
SplashScreen::~SplashScreen() {} | |||
void SplashScreen::deleteAfterDelay (RelativeTime timeout, bool removeOnMouseClick) | |||
{ | |||
// Note that this method must be safe to call from non-GUI threads | |||
if (! removeOnMouseClick) | |||
clickCountToDelete = std::numeric_limits<int>::max(); | |||
minimumVisibleTime = timeout; | |||
startTimer (50); | |||
} | |||
void SplashScreen::paint (Graphics& g) | |||
{ | |||
g.setOpacity (1.0f); | |||
g.drawImage (backgroundImage, getLocalBounds().toFloat(), RectanglePlacement (RectanglePlacement::fillDestination)); | |||
} | |||
void SplashScreen::timerCallback() | |||
{ | |||
if (Time::getCurrentTime() > creationTime + minimumVisibleTime | |||
|| Desktop::getInstance().getMouseButtonClickCounter() > clickCountToDelete) | |||
delete this; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,155 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** A component for showing a splash screen while your app starts up. | |||
This will automatically position itself, and can be told to delete itself after | |||
being on-screen for a minimum length of time. | |||
To use it, just create one of these in your JUCEApplicationBase::initialise() method, | |||
and when your initialisation tasks have finished running, call its deleteAfterDelay() | |||
method to make it automatically get rid of itself. | |||
Note that although you could call deleteAfterDelay() as soon as you create the | |||
SplashScreen object, if you've got a long initialisation procedure, you probably | |||
don't want the splash to time-out and disappear before the initialisation has | |||
finished, which is why it makes sense to not call this method until the end of | |||
your init tasks. | |||
E.g. @code | |||
void MyApp::initialise (const String& commandLine) | |||
{ | |||
splash = new SplashScreen ("Welcome to my app!", | |||
ImageFileFormat::loadFrom (File ("/foobar/splash.jpg")), | |||
true); | |||
// now kick off your initialisation work on some kind of thread or task, and | |||
launchBackgroundInitialisationThread(); | |||
} | |||
void MyApp::myInitialisationWorkFinished() | |||
{ | |||
// ..assuming this is some kind of callback method that is triggered when | |||
// your background initialisation threads have finished, and it's time to open | |||
// your main window, etc.. | |||
splash->deleteAfterDelay (RelativeTime::seconds (4), false); | |||
...etc... | |||
} | |||
@endcode | |||
*/ | |||
class JUCE_API SplashScreen : public Component, | |||
private Timer, | |||
private DeletedAtShutdown | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates a SplashScreen object. | |||
When called, the constructor will position the SplashScreen in the centre of the | |||
display, and after the time specified, it will automatically delete itself. | |||
Bear in mind that if you call this during your JUCEApplicationBase::initialise() | |||
method and then block the message thread by performing some kind of task, then | |||
obviously neither your splash screen nor any other GUI will appear until you | |||
allow the message thread to resume and do its work. So if you have time-consuming | |||
tasks to do during startup, use a background thread for them. | |||
After creating one of these (or your subclass of it), you should do your app's | |||
initialisation work, and then call the deleteAfterDelay() method to tell this object | |||
to delete itself after the user has had chance to get a good look at it. | |||
If you're writing a custom splash screen class, there's another protected constructor | |||
that your subclass can call, which doesn't take an image. | |||
@param title the name to give the component | |||
@param backgroundImage an image to draw on the component. The component's size | |||
will be set to the size of this image, and if the image is | |||
semi-transparent, the component will be made non-opaque | |||
@param useDropShadow if true, the window will have a drop shadow | |||
*/ | |||
SplashScreen (const String& title, | |||
const Image& backgroundImage, | |||
bool useDropShadow); | |||
/** Destructor. */ | |||
~SplashScreen(); | |||
/** Tells the component to auto-delete itself after a timeout period, or when the | |||
mouse is clicked. | |||
You should call this after finishing your app's initialisation work. | |||
Note that although you could call deleteAfterDelay() as soon as you create the | |||
SplashScreen object, if you've got a long initialisation procedure, you probably | |||
don't want the splash to time-out and disappear before your initialisation has | |||
finished, which is why it makes sense to not call this method and start the | |||
self-delete timer until you're ready. | |||
It's safe to call this method from a non-GUI thread as long as there's no danger that | |||
the object may be being deleted at the same time. | |||
@param minimumTotalTimeToDisplayFor how long the splash screen should stay visible for. | |||
Note that this time is measured from the construction-time of this | |||
object, not from the time that the deleteAfterDelay() method is | |||
called, so if you call this method after a long initialisation | |||
period, it may be deleted without any further delay. | |||
@param removeOnMouseClick if true, the window will be deleted as soon as the user clicks | |||
the mouse (anywhere) | |||
*/ | |||
void deleteAfterDelay (RelativeTime minimumTotalTimeToDisplayFor, | |||
bool removeOnMouseClick); | |||
protected: | |||
//============================================================================== | |||
/** This constructor is for use by custom sub-classes that don't want to provide an image. */ | |||
SplashScreen (const String& title, int width, int height, bool useDropShadow); | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
private: | |||
//============================================================================== | |||
Image backgroundImage; | |||
Time creationTime; | |||
RelativeTime minimumVisibleTime; | |||
int clickCountToDelete; | |||
void timerCallback() override; | |||
void makeVisible (int w, int h, bool shadow, bool fullscreen); | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SplashScreen) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,43 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_WINDOWS || JUCE_LINUX || JUCE_MAC | |||
SystemTrayIconComponent::SystemTrayIconComponent() | |||
{ | |||
addToDesktop (0); | |||
} | |||
SystemTrayIconComponent::~SystemTrayIconComponent() | |||
{ | |||
} | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,105 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_WINDOWS || JUCE_LINUX || JUCE_MAC || DOXYGEN | |||
//============================================================================== | |||
/** | |||
This component sits in the taskbar tray as a small icon. | |||
(NB: The exact behaviour of this class will differ between OSes, and it | |||
isn't fully implemented for all OSes) | |||
To use it, just create one of these components, but don't attempt to make it | |||
visible, add it to a parent, or put it on the desktop. | |||
You can then call setIconImage() to create an icon for it in the taskbar. | |||
To change the icon's tooltip, you can use setIconTooltip(). | |||
To respond to mouse-events, you can override the normal mouseDown(), | |||
mouseUp(), mouseDoubleClick() and mouseMove() methods, and although the x, y | |||
position will not be valid, you can use this to respond to clicks. Traditionally | |||
you'd use a left-click to show your application's window, and a right-click | |||
to show a pop-up menu. | |||
*/ | |||
class JUCE_API SystemTrayIconComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
SystemTrayIconComponent(); | |||
/** Destructor. */ | |||
~SystemTrayIconComponent(); | |||
//============================================================================== | |||
/** Changes the image shown in the taskbar. */ | |||
void setIconImage (const Image& newImage); | |||
/** Changes the icon's tooltip (if the current OS supports this). */ | |||
void setIconTooltip (const String& tooltip); | |||
/** Highlights the icon (if the current OS supports this). */ | |||
void setHighlighted (bool); | |||
/** Shows a floating text bubble pointing to the icon (if the current OS supports this). */ | |||
void showInfoBubble (const String& title, const String& content); | |||
/** Hides the icon's floating text bubble (if the current OS supports this). */ | |||
void hideInfoBubble(); | |||
/** Returns the raw handle to whatever kind of internal OS structure is | |||
involved in showing this icon. | |||
@see ComponentPeer::getNativeHandle() | |||
*/ | |||
void* getNativeHandle() const; | |||
#if JUCE_LINUX | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
#endif | |||
#if JUCE_MAC | |||
/** Shows a menu attached to the OSX menu bar icon. */ | |||
void showDropdownMenu (const PopupMenu& menu); | |||
#endif | |||
private: | |||
//============================================================================== | |||
JUCE_PUBLIC_IN_DLL_BUILD (class Pimpl) | |||
ScopedPointer<Pimpl> pimpl; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SystemTrayIconComponent) | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,154 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#if JUCE_WEB_BROWSER || DOXYGEN | |||
//============================================================================== | |||
/** | |||
A component that displays an embedded web browser. | |||
The browser itself will be platform-dependent. On the Mac, probably Safari, on | |||
Windows, probably IE. | |||
*/ | |||
class JUCE_API WebBrowserComponent : public Component | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Creates a WebBrowserComponent. | |||
Once it's created and visible, send the browser to a URL using goToURL(). | |||
@param unloadPageWhenBrowserIsHidden if this is true, then when the browser | |||
component is taken offscreen, it'll clear the current page | |||
and replace it with a blank page - this can be handy to stop | |||
the browser using resources in the background when it's not | |||
actually being used. | |||
*/ | |||
explicit WebBrowserComponent (bool unloadPageWhenBrowserIsHidden = true); | |||
/** Destructor. */ | |||
~WebBrowserComponent(); | |||
//============================================================================== | |||
/** Sends the browser to a particular URL. | |||
@param url the URL to go to. | |||
@param headers an optional set of parameters to put in the HTTP header. If | |||
you supply this, it should be a set of string in the form | |||
"HeaderKey: HeaderValue" | |||
@param postData an optional block of data that will be attached to the HTTP | |||
POST request | |||
*/ | |||
void goToURL (const String& url, | |||
const StringArray* headers = nullptr, | |||
const MemoryBlock* postData = nullptr); | |||
/** Stops the current page loading. */ | |||
void stop(); | |||
/** Sends the browser back one page. */ | |||
void goBack(); | |||
/** Sends the browser forward one page. */ | |||
void goForward(); | |||
/** Refreshes the browser. */ | |||
void refresh(); | |||
/** Clear cookies that the OS has stored for the WebComponents of this application */ | |||
static void clearCookies(); | |||
//============================================================================== | |||
/** This callback is called when the browser is about to navigate | |||
to a new location. | |||
You can override this method to perform some action when the user | |||
tries to go to a particular URL. To allow the operation to carry on, | |||
return true, or return false to stop the navigation happening. | |||
*/ | |||
virtual bool pageAboutToLoad (const String& newURL); | |||
/** This callback happens when the browser has finished loading a page. */ | |||
virtual void pageFinishedLoading (const String& url); | |||
/** This callback happens when a network error was encountered while | |||
trying to load a page. | |||
You can override this method to show some other error page by calling | |||
goToURL. Return true to allow the browser to carry on to the internal | |||
browser error page. | |||
The errorInfo contains some platform dependent string describing the | |||
error. | |||
*/ | |||
virtual bool pageLoadHadNetworkError (const String& errorInfo); | |||
/** This callback occurs when a script or other activity in the browser asks for | |||
the window to be closed. | |||
*/ | |||
virtual void windowCloseRequest(); | |||
/** This callback occurs when the browser attempts to load a URL in a new window. | |||
This won't actually load the window but gives you a chance to either launch a | |||
new window yourself or just load the URL into the current window with goToURL(). | |||
*/ | |||
virtual void newWindowAttemptingToLoad (const String& newURL); | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
void resized() override; | |||
/** @internal */ | |||
void parentHierarchyChanged() override; | |||
/** @internal */ | |||
void visibilityChanged() override; | |||
/** @internal */ | |||
void focusGained (FocusChangeType) override; | |||
private: | |||
//============================================================================== | |||
class Pimpl; | |||
Pimpl* browser; | |||
bool blankPageShown, unloadPageWhenBrowserIsHidden; | |||
String lastURL; | |||
StringArray lastHeaders; | |||
MemoryBlock lastPostData; | |||
void reloadLastURL(); | |||
void checkWindowAssociation(); | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebBrowserComponent) | |||
}; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,128 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
WebBrowserComponent::WebBrowserComponent (const bool unloadPageWhenBrowserIsHidden_) | |||
: browser (nullptr), | |||
blankPageShown (false), | |||
unloadPageWhenBrowserIsHidden (unloadPageWhenBrowserIsHidden_) | |||
{ | |||
// Unfortunately, WebBrowserComponent is not implemented for Android yet! | |||
// This is just a stub implementation without any useful functionality. | |||
jassertfalse; | |||
setOpaque (true); | |||
} | |||
WebBrowserComponent::~WebBrowserComponent() | |||
{ | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::goToURL (const String& url, | |||
const StringArray* headers, | |||
const MemoryBlock* postData) | |||
{ | |||
lastURL = url; | |||
if (headers != nullptr) | |||
lastHeaders = *headers; | |||
else | |||
lastHeaders.clear(); | |||
if (postData != nullptr) | |||
lastPostData = *postData; | |||
else | |||
lastPostData.reset(); | |||
blankPageShown = false; | |||
} | |||
void WebBrowserComponent::stop() | |||
{ | |||
} | |||
void WebBrowserComponent::goBack() | |||
{ | |||
lastURL.clear(); | |||
blankPageShown = false; | |||
} | |||
void WebBrowserComponent::goForward() | |||
{ | |||
lastURL.clear(); | |||
} | |||
void WebBrowserComponent::refresh() | |||
{ | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::paint (Graphics& g) | |||
{ | |||
g.fillAll (Colours::white); | |||
} | |||
void WebBrowserComponent::checkWindowAssociation() | |||
{ | |||
} | |||
void WebBrowserComponent::reloadLastURL() | |||
{ | |||
if (lastURL.isNotEmpty()) | |||
{ | |||
goToURL (lastURL, &lastHeaders, &lastPostData); | |||
lastURL.clear(); | |||
} | |||
} | |||
void WebBrowserComponent::parentHierarchyChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::resized() | |||
{ | |||
} | |||
void WebBrowserComponent::visibilityChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::focusGained (FocusChangeType) | |||
{ | |||
} | |||
void WebBrowserComponent::clearCookies() | |||
{ | |||
} | |||
} // namespace juce |
@@ -0,0 +1,133 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class UIViewComponent::Pimpl : public ComponentMovementWatcher | |||
{ | |||
public: | |||
Pimpl (UIView* v, Component& comp) | |||
: ComponentMovementWatcher (&comp), | |||
view (v), | |||
owner (comp) | |||
{ | |||
[view retain]; | |||
if (owner.isShowing()) | |||
componentPeerChanged(); | |||
} | |||
~Pimpl() | |||
{ | |||
[view removeFromSuperview]; | |||
[view release]; | |||
} | |||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override | |||
{ | |||
auto* topComp = owner.getTopLevelComponent(); | |||
if (topComp->getPeer() != nullptr) | |||
{ | |||
auto pos = topComp->getLocalPoint (&owner, Point<int>()); | |||
[view setFrame: CGRectMake ((float) pos.x, (float) pos.y, | |||
(float) owner.getWidth(), (float) owner.getHeight())]; | |||
} | |||
} | |||
void componentPeerChanged() override | |||
{ | |||
auto* peer = owner.getPeer(); | |||
if (currentPeer != peer) | |||
{ | |||
if ([view superview] != nil) | |||
[view removeFromSuperview]; // Must be careful not to call this unless it's required - e.g. some Apple AU views | |||
// override the call and use it as a sign that they're being deleted, which breaks everything.. | |||
currentPeer = peer; | |||
if (peer != nullptr) | |||
{ | |||
UIView* peerView = (UIView*) peer->getNativeHandle(); | |||
[peerView addSubview: view]; | |||
componentMovedOrResized (false, false); | |||
} | |||
} | |||
[view setHidden: ! owner.isShowing()]; | |||
} | |||
void componentVisibilityChanged() override | |||
{ | |||
componentPeerChanged(); | |||
} | |||
Rectangle<int> getViewBounds() const | |||
{ | |||
CGRect r = [view frame]; | |||
return Rectangle<int> ((int) r.size.width, (int) r.size.height); | |||
} | |||
UIView* const view; | |||
private: | |||
Component& owner; | |||
ComponentPeer* currentPeer = nullptr; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | |||
}; | |||
//============================================================================== | |||
UIViewComponent::UIViewComponent() {} | |||
UIViewComponent::~UIViewComponent() {} | |||
void UIViewComponent::setView (void* const view) | |||
{ | |||
if (view != getView()) | |||
{ | |||
pimpl = nullptr; | |||
if (view != nullptr) | |||
pimpl = new Pimpl ((UIView*) view, *this); | |||
} | |||
} | |||
void* UIViewComponent::getView() const | |||
{ | |||
return pimpl == nullptr ? nullptr : pimpl->view; | |||
} | |||
void UIViewComponent::resizeToFitView() | |||
{ | |||
if (pimpl != nullptr) | |||
setBounds (pimpl->getViewBounds()); | |||
} | |||
void UIViewComponent::paint (Graphics&) {} | |||
} // namespace juce |
@@ -0,0 +1,149 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class SystemTrayIconComponent::Pimpl | |||
{ | |||
public: | |||
Pimpl (const Image& im, Window windowH) : image (im) | |||
{ | |||
ScopedXDisplay xDisplay; | |||
auto display = xDisplay.display; | |||
ScopedXLock xlock (display); | |||
Screen* const screen = XDefaultScreenOfDisplay (display); | |||
const int screenNumber = XScreenNumberOfScreen (screen); | |||
String screenAtom ("_NET_SYSTEM_TRAY_S"); | |||
screenAtom << screenNumber; | |||
Atom selectionAtom = Atoms::getCreating (display, screenAtom.toUTF8()); | |||
XGrabServer (display); | |||
Window managerWin = XGetSelectionOwner (display, selectionAtom); | |||
if (managerWin != None) | |||
XSelectInput (display, managerWin, StructureNotifyMask); | |||
XUngrabServer (display); | |||
XFlush (display); | |||
if (managerWin != None) | |||
{ | |||
XEvent ev = { 0 }; | |||
ev.xclient.type = ClientMessage; | |||
ev.xclient.window = managerWin; | |||
ev.xclient.message_type = Atoms::getCreating (display, "_NET_SYSTEM_TRAY_OPCODE"); | |||
ev.xclient.format = 32; | |||
ev.xclient.data.l[0] = CurrentTime; | |||
ev.xclient.data.l[1] = 0 /*SYSTEM_TRAY_REQUEST_DOCK*/; | |||
ev.xclient.data.l[2] = (long) windowH; | |||
ev.xclient.data.l[3] = 0; | |||
ev.xclient.data.l[4] = 0; | |||
XSendEvent (display, managerWin, False, NoEventMask, &ev); | |||
XSync (display, False); | |||
} | |||
// For older KDE's ... | |||
long atomData = 1; | |||
Atom trayAtom = Atoms::getCreating (display, "KWM_DOCKWINDOW"); | |||
XChangeProperty (display, windowH, trayAtom, trayAtom, 32, PropModeReplace, (unsigned char*) &atomData, 1); | |||
// For more recent KDE's... | |||
trayAtom = Atoms::getCreating (display, "_KDE_NET_WM_SYSTEM_TRAY_WINDOW_FOR"); | |||
XChangeProperty (display, windowH, trayAtom, XA_WINDOW, 32, PropModeReplace, (unsigned char*) &windowH, 1); | |||
// A minimum size must be specified for GNOME and Xfce, otherwise the icon is displayed with a width of 1 | |||
XSizeHints* hints = XAllocSizeHints(); | |||
hints->flags = PMinSize; | |||
hints->min_width = 22; | |||
hints->min_height = 22; | |||
XSetWMNormalHints (display, windowH, hints); | |||
XFree (hints); | |||
} | |||
Image image; | |||
private: | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | |||
}; | |||
//============================================================================== | |||
void SystemTrayIconComponent::setIconImage (const Image& newImage) | |||
{ | |||
pimpl = nullptr; | |||
if (newImage.isValid()) | |||
{ | |||
if (! isOnDesktop()) | |||
addToDesktop (0); | |||
pimpl = new Pimpl (newImage, (Window) getWindowHandle()); | |||
setVisible (true); | |||
toFront (false); | |||
} | |||
repaint(); | |||
} | |||
void SystemTrayIconComponent::paint (Graphics& g) | |||
{ | |||
if (pimpl != nullptr) | |||
g.drawImage (pimpl->image, getLocalBounds().toFloat(), | |||
RectanglePlacement::xLeft | RectanglePlacement::yTop | RectanglePlacement::onlyReduceInSize); | |||
} | |||
void SystemTrayIconComponent::setIconTooltip (const String& /*tooltip*/) | |||
{ | |||
// xxx Not implemented! | |||
} | |||
void SystemTrayIconComponent::setHighlighted (bool) | |||
{ | |||
// xxx Not implemented! | |||
} | |||
void SystemTrayIconComponent::showInfoBubble (const String& /*title*/, const String& /*content*/) | |||
{ | |||
// xxx Not implemented! | |||
} | |||
void SystemTrayIconComponent::hideInfoBubble() | |||
{ | |||
// xxx Not implemented! | |||
} | |||
void* SystemTrayIconComponent::getNativeHandle() const | |||
{ | |||
return getWindowHandle(); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,827 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
extern int juce_gtkWebkitMain (int argc, const char* argv[]); | |||
class CommandReceiver | |||
{ | |||
public: | |||
struct Responder | |||
{ | |||
virtual ~Responder() {} | |||
virtual void handleCommand (const String& cmd, const var& param) = 0; | |||
virtual void receiverHadError() = 0; | |||
}; | |||
CommandReceiver (Responder* responderToUse, int inputChannelToUse) | |||
: responder (responderToUse), inChannel (inputChannelToUse) | |||
{ | |||
setBlocking (inChannel, false); | |||
} | |||
static void setBlocking (int fd, bool shouldBlock) | |||
{ | |||
int flags = fcntl (fd, F_GETFL); | |||
fcntl (fd, F_SETFL, (shouldBlock ? (flags & ~O_NONBLOCK) | |||
: (flags | O_NONBLOCK))); | |||
} | |||
int getFd() const { return inChannel; } | |||
void tryNextRead() | |||
{ | |||
while (true) | |||
{ | |||
size_t len = (receivingLength ? sizeof (size_t) : bufferLength.len); | |||
if (! receivingLength) | |||
buffer.realloc (len); | |||
char* dst = (receivingLength ? bufferLength.data : buffer.getData()); | |||
ssize_t actual = read (inChannel, &dst[pos], static_cast<size_t> (len - pos)); | |||
if (actual < 0) | |||
{ | |||
if (errno == EINTR) | |||
continue; | |||
break; | |||
} | |||
pos += static_cast<size_t> (actual); | |||
if (pos == len) | |||
{ | |||
pos = 0; | |||
if (! receivingLength) | |||
parseJSON (String (buffer.getData(), bufferLength.len)); | |||
receivingLength = (! receivingLength); | |||
} | |||
} | |||
if (errno != EAGAIN && errno != EWOULDBLOCK && responder != nullptr) | |||
responder->receiverHadError(); | |||
} | |||
static void sendCommand (int outChannel, const String& cmd, const var& params) | |||
{ | |||
DynamicObject::Ptr obj = new DynamicObject; | |||
obj->setProperty (getCmdIdentifier(), cmd); | |||
if (! params.isVoid()) | |||
obj->setProperty (getParamIdentifier(), params); | |||
String json (JSON::toString (var (obj))); | |||
size_t jsonLength = static_cast<size_t> (json.length()); | |||
size_t len = sizeof (size_t) + jsonLength; | |||
HeapBlock<char> buffer (len); | |||
char* dst = buffer.getData(); | |||
memcpy (dst, &jsonLength, sizeof (size_t)); | |||
dst += sizeof (size_t); | |||
memcpy (dst, json.toRawUTF8(), jsonLength); | |||
ssize_t ret; | |||
do | |||
{ | |||
ret = write (outChannel, buffer.getData(), len); | |||
} while (ret == -1 && errno == EINTR); | |||
} | |||
private: | |||
void parseJSON (const String& json) | |||
{ | |||
var object (JSON::fromString (json)); | |||
if (! object.isVoid()) | |||
{ | |||
String cmd (object.getProperty (getCmdIdentifier(), var()).toString()); | |||
var params (object.getProperty (getParamIdentifier(), var())); | |||
if (responder != nullptr) | |||
responder->handleCommand (cmd, params); | |||
} | |||
} | |||
static Identifier getCmdIdentifier() { static Identifier Id ("cmd"); return Id; } | |||
static Identifier getParamIdentifier() { static Identifier Id ("params"); return Id; } | |||
Responder* responder; | |||
int inChannel; | |||
size_t pos = 0; | |||
bool receivingLength = true; | |||
union { char data [sizeof (size_t)]; size_t len; } bufferLength; | |||
HeapBlock<char> buffer; | |||
}; | |||
//============================================================================== | |||
class GtkChildProcess : private CommandReceiver::Responder | |||
{ | |||
public: | |||
//============================================================================== | |||
GtkChildProcess (int inChannel, int outChannelToUse) | |||
: outChannel (outChannelToUse), receiver (this, inChannel) | |||
{} | |||
typedef void (*SetHardwareAcclPolicyFunctionPtr) (WebKitSettings*, int); | |||
int entry() | |||
{ | |||
CommandReceiver::setBlocking (outChannel, true); | |||
gtk_init (nullptr, nullptr); | |||
WebKitSettings* settings = webkit_settings_new(); | |||
// webkit_settings_set_hardware_acceleration_policy was only added recently to webkit2 | |||
// but is needed when running a WebBrowserComponent in a Parallels VM with 3D acceleration enabled | |||
auto setHardwarePolicy | |||
= reinterpret_cast<SetHardwareAcclPolicyFunctionPtr> (dlsym (RTLD_DEFAULT, "webkit_settings_set_hardware_acceleration_policy")); | |||
if (setHardwarePolicy != nullptr) | |||
setHardwarePolicy (settings, 2 /*WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER*/); | |||
GtkWidget *plug; | |||
plug = gtk_plug_new(0); | |||
GtkWidget* container; | |||
container = gtk_scrolled_window_new (nullptr, nullptr); | |||
GtkWidget* webviewWidget = webkit_web_view_new_with_settings (settings); | |||
webview = WEBKIT_WEB_VIEW (webviewWidget); | |||
gtk_container_add (GTK_CONTAINER (container), webviewWidget); | |||
gtk_container_add (GTK_CONTAINER (plug), container); | |||
webkit_web_view_load_uri (webview, "about:blank"); | |||
g_signal_connect (webview, "decide-policy", | |||
G_CALLBACK (decidePolicyCallback), this); | |||
g_signal_connect (webview, "load-changed", | |||
G_CALLBACK (loadChangedCallback), this); | |||
g_signal_connect (webview, "load-failed", | |||
G_CALLBACK (loadFailedCallback), this); | |||
gtk_widget_show_all (plug); | |||
unsigned long wID = (unsigned long) gtk_plug_get_id (GTK_PLUG (plug)); | |||
ssize_t ret; | |||
do { | |||
ret = write (outChannel, &wID, sizeof (wID)); | |||
} while (ret == -1 && errno == EINTR); | |||
g_unix_fd_add (receiver.getFd(), G_IO_IN, pipeReadyStatic, this); | |||
receiver.tryNextRead(); | |||
gtk_main(); | |||
return 0; | |||
} | |||
void goToURL (const var& params) | |||
{ | |||
static Identifier urlIdentifier ("url"); | |||
String url (params.getProperty (urlIdentifier, var()).toString()); | |||
webkit_web_view_load_uri (webview, url.toRawUTF8()); | |||
} | |||
void handleDecisionResponse (const var& params) | |||
{ | |||
WebKitPolicyDecision* decision | |||
= (WebKitPolicyDecision*) ((int64) params.getProperty ("decision_id", var (0))); | |||
bool allow = params.getProperty ("allow", var (false)); | |||
if (decision != nullptr && decisions.contains (decision)) | |||
{ | |||
if (allow) | |||
webkit_policy_decision_use (decision); | |||
else | |||
webkit_policy_decision_ignore (decision); | |||
decisions.removeAllInstancesOf (decision); | |||
g_object_unref (decision); | |||
} | |||
} | |||
//============================================================================== | |||
void handleCommand (const String& cmd, const var& params) override | |||
{ | |||
if (cmd == "quit") quit(); | |||
else if (cmd == "goToURL") goToURL (params); | |||
else if (cmd == "goBack") webkit_web_view_go_back (webview); | |||
else if (cmd == "goForward") webkit_web_view_go_forward (webview); | |||
else if (cmd == "refresh") webkit_web_view_reload (webview); | |||
else if (cmd == "stop") webkit_web_view_stop_loading (webview); | |||
else if (cmd == "decision") handleDecisionResponse (params); | |||
} | |||
void receiverHadError() override | |||
{ | |||
exit (-1); | |||
} | |||
//============================================================================== | |||
bool pipeReady (gint fd, GIOCondition) | |||
{ | |||
if (fd == receiver.getFd()) | |||
{ | |||
receiver.tryNextRead(); | |||
return true; | |||
} | |||
return false; | |||
} | |||
void quit() | |||
{ | |||
gtk_main_quit(); | |||
} | |||
bool onNavigation (String frameName, | |||
WebKitNavigationAction* action, | |||
WebKitPolicyDecision* decision) | |||
{ | |||
if (decision != nullptr && frameName.isEmpty()) | |||
{ | |||
g_object_ref (decision); | |||
decisions.add (decision); | |||
DynamicObject::Ptr params = new DynamicObject; | |||
params->setProperty ("url", String (webkit_uri_request_get_uri (webkit_navigation_action_get_request (action)))); | |||
params->setProperty ("decision_id", (int64) decision); | |||
CommandReceiver::sendCommand (outChannel, "pageAboutToLoad", var (params)); | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool onNewWindow (String /*frameName*/, | |||
WebKitNavigationAction* action, | |||
WebKitPolicyDecision* decision) | |||
{ | |||
if (decision != nullptr) | |||
{ | |||
DynamicObject::Ptr params = new DynamicObject; | |||
params->setProperty ("url", String (webkit_uri_request_get_uri (webkit_navigation_action_get_request (action)))); | |||
CommandReceiver::sendCommand (outChannel, "newWindowAttemptingToLoad", var (params)); | |||
// never allow new windows | |||
webkit_policy_decision_ignore (decision); | |||
return true; | |||
} | |||
return false; | |||
} | |||
void onLoadChanged (WebKitLoadEvent loadEvent) | |||
{ | |||
if (loadEvent == WEBKIT_LOAD_FINISHED) | |||
{ | |||
DynamicObject::Ptr params = new DynamicObject; | |||
params->setProperty ("url", String (webkit_web_view_get_uri (webview))); | |||
CommandReceiver::sendCommand (outChannel, "pageFinishedLoading", var (params)); | |||
} | |||
} | |||
bool onDecidePolicy (WebKitPolicyDecision* decision, | |||
WebKitPolicyDecisionType decisionType) | |||
{ | |||
switch (decisionType) | |||
{ | |||
case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION: | |||
{ | |||
WebKitNavigationPolicyDecision* navigationDecision = WEBKIT_NAVIGATION_POLICY_DECISION (decision); | |||
const char* frameName = webkit_navigation_policy_decision_get_frame_name (navigationDecision); | |||
return onNavigation (String (frameName != nullptr ? frameName : ""), | |||
webkit_navigation_policy_decision_get_navigation_action (navigationDecision), | |||
decision); | |||
} | |||
break; | |||
case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION: | |||
{ | |||
WebKitNavigationPolicyDecision* navigationDecision = WEBKIT_NAVIGATION_POLICY_DECISION (decision); | |||
const char* frameName = webkit_navigation_policy_decision_get_frame_name (navigationDecision); | |||
return onNewWindow (String (frameName != nullptr ? frameName : ""), | |||
webkit_navigation_policy_decision_get_navigation_action (navigationDecision), | |||
decision); | |||
} | |||
break; | |||
case WEBKIT_POLICY_DECISION_TYPE_RESPONSE: | |||
{ | |||
WebKitResponsePolicyDecision *response = WEBKIT_RESPONSE_POLICY_DECISION (decision); | |||
// for now just always allow response requests | |||
ignoreUnused (response); | |||
webkit_policy_decision_use (decision); | |||
return true; | |||
} | |||
break; | |||
default: | |||
break; | |||
} | |||
return false; | |||
} | |||
void onLoadFailed (GError* error) | |||
{ | |||
DynamicObject::Ptr params = new DynamicObject; | |||
params->setProperty ("error", String (error != nullptr ? error->message : "unknown error")); | |||
CommandReceiver::sendCommand (outChannel, "pageLoadHadNetworkError", var (params)); | |||
} | |||
private: | |||
static gboolean pipeReadyStatic (gint fd, GIOCondition condition, gpointer user) | |||
{ | |||
return (reinterpret_cast<GtkChildProcess*> (user)->pipeReady (fd, condition) ? TRUE : FALSE); | |||
} | |||
static gboolean decidePolicyCallback (WebKitWebView*, | |||
WebKitPolicyDecision* decision, | |||
WebKitPolicyDecisionType decisionType, | |||
gpointer user) | |||
{ | |||
GtkChildProcess& owner = *reinterpret_cast<GtkChildProcess*> (user); | |||
return (owner.onDecidePolicy (decision, decisionType) ? TRUE : FALSE); | |||
} | |||
static void loadChangedCallback (WebKitWebView*, | |||
WebKitLoadEvent loadEvent, | |||
gpointer user) | |||
{ | |||
GtkChildProcess& owner = *reinterpret_cast<GtkChildProcess*> (user); | |||
owner.onLoadChanged (loadEvent); | |||
} | |||
static void loadFailedCallback (WebKitWebView*, | |||
WebKitLoadEvent /*loadEvent*/, | |||
gchar* /*failing_uri*/, | |||
GError* error, | |||
gpointer user) | |||
{ | |||
GtkChildProcess& owner = *reinterpret_cast<GtkChildProcess*> (user); | |||
owner.onLoadFailed (error); | |||
} | |||
int outChannel; | |||
CommandReceiver receiver; | |||
WebKitWebView* webview = nullptr; | |||
Array<WebKitPolicyDecision*> decisions; | |||
}; | |||
//============================================================================== | |||
class WebBrowserComponent::Pimpl : private Thread, private CommandReceiver::Responder | |||
{ | |||
public: | |||
Pimpl (WebBrowserComponent& parent) | |||
: Thread ("Webview"), owner (parent) | |||
{} | |||
~Pimpl() | |||
{ | |||
quit(); | |||
} | |||
//============================================================================== | |||
void init() | |||
{ | |||
launchChild(); | |||
int ret = pipe (threadControl); | |||
ignoreUnused (ret); | |||
jassert (ret == 0); | |||
CommandReceiver::setBlocking (inChannel, true); | |||
CommandReceiver::setBlocking (outChannel, true); | |||
CommandReceiver::setBlocking (threadControl[0], false); | |||
CommandReceiver::setBlocking (threadControl[1], true); | |||
unsigned long windowHandle; | |||
ssize_t actual = read (inChannel, &windowHandle, sizeof (windowHandle)); | |||
if (actual != sizeof (windowHandle)) | |||
{ | |||
killChild(); | |||
return; | |||
} | |||
receiver = new CommandReceiver (this, inChannel); | |||
startThread(); | |||
xembed = new XEmbedComponent (windowHandle); | |||
owner.addAndMakeVisible (xembed); | |||
} | |||
void quit() | |||
{ | |||
if (isThreadRunning()) | |||
{ | |||
signalThreadShouldExit(); | |||
char ignore = 0; | |||
ssize_t ret; | |||
do | |||
{ | |||
ret = write (threadControl[1], &ignore, 1); | |||
} while (ret == -1 && errno == EINTR); | |||
waitForThreadToExit (-1); | |||
receiver = nullptr; | |||
} | |||
if (childProcess != 0) | |||
{ | |||
CommandReceiver::sendCommand (outChannel, "quit", var()); | |||
killChild(); | |||
} | |||
} | |||
//============================================================================== | |||
void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) | |||
{ | |||
DynamicObject::Ptr params = new DynamicObject; | |||
params->setProperty ("url", url); | |||
if (headers != nullptr) | |||
params->setProperty ("headers", var (*headers)); | |||
if (postData != nullptr) | |||
params->setProperty ("postData", var (*postData)); | |||
CommandReceiver::sendCommand (outChannel, "goToURL", var (params)); | |||
} | |||
void goBack() { CommandReceiver::sendCommand (outChannel, "goBack", var()); } | |||
void goForward() { CommandReceiver::sendCommand (outChannel, "goForward", var()); } | |||
void refresh() { CommandReceiver::sendCommand (outChannel, "refresh", var()); } | |||
void stop() { CommandReceiver::sendCommand (outChannel, "stop", var()); } | |||
void resized() | |||
{ | |||
if (xembed != nullptr) | |||
xembed->setBounds (owner.getLocalBounds()); | |||
} | |||
private: | |||
//============================================================================== | |||
void killChild() | |||
{ | |||
if (childProcess != 0) | |||
{ | |||
xembed = nullptr; | |||
int status = 0, result; | |||
result = waitpid (childProcess, &status, WNOHANG); | |||
for (int i = 0; i < 15 && (! WIFEXITED(status) || result != childProcess); ++i) | |||
{ | |||
Thread::sleep (100); | |||
result = waitpid (childProcess, &status, WNOHANG); | |||
} | |||
// clean-up any zombies | |||
status = 0; | |||
if (! WIFEXITED(status) || result != childProcess) | |||
{ | |||
do | |||
{ | |||
kill (childProcess, SIGTERM); | |||
waitpid (childProcess, &status, 0); | |||
} while (! WIFEXITED(status)); | |||
} | |||
childProcess = 0; | |||
} | |||
} | |||
void launchChild() | |||
{ | |||
int ret; | |||
int inPipe[2], outPipe[2]; | |||
ret = pipe (inPipe); | |||
ignoreUnused (ret); jassert (ret == 0); | |||
ret = pipe (outPipe); | |||
ignoreUnused (ret); jassert (ret == 0); | |||
int pid = fork(); | |||
if (pid == 0) | |||
{ | |||
close (inPipe[0]); | |||
close (outPipe[1]); | |||
HeapBlock<const char*> argv (5); | |||
StringArray arguments; | |||
arguments.add (File::getSpecialLocation (File::currentExecutableFile).getFullPathName()); | |||
arguments.add ("--juce-gtkwebkitfork-child"); | |||
arguments.add (String (outPipe[0])); | |||
arguments.add (String (inPipe [1])); | |||
for (int i = 0; i < arguments.size(); ++i) | |||
argv[i] = arguments[i].toRawUTF8(); | |||
argv[4] = nullptr; | |||
#if JUCE_STANDALONE_APPLICATION | |||
execv (arguments[0].toRawUTF8(), (char**) argv.getData()); | |||
#else | |||
juce_gtkWebkitMain (4, (const char**) argv.getData()); | |||
#endif | |||
exit (0); | |||
} | |||
close (inPipe[1]); | |||
close (outPipe[0]); | |||
inChannel = inPipe[0]; | |||
outChannel = outPipe[1]; | |||
childProcess = pid; | |||
} | |||
void run() override | |||
{ | |||
while (! threadShouldExit()) | |||
{ | |||
if (shouldExit()) | |||
return; | |||
receiver->tryNextRead(); | |||
fd_set set; | |||
FD_ZERO (&set); | |||
FD_SET (threadControl[0], &set); | |||
FD_SET (receiver->getFd(), &set); | |||
int max_fd = jmax (threadControl[0], receiver->getFd()); | |||
int result = 0; | |||
while (result == 0 || (result < 0 && errno == EINTR)) | |||
result = select (max_fd + 1, &set, NULL, NULL, NULL); | |||
if (result < 0) | |||
break; | |||
} | |||
} | |||
bool shouldExit() | |||
{ | |||
char ignore; | |||
ssize_t result = read (threadControl[0], &ignore, 1); | |||
return (result != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)); | |||
} | |||
//============================================================================== | |||
void handleCommandOnMessageThread (const String& cmd, const var& params) | |||
{ | |||
String url (params.getProperty ("url", var()).toString()); | |||
if (cmd == "pageAboutToLoad") handlePageAboutToLoad (url, params); | |||
else if (cmd == "pageFinishedLoading") owner.pageFinishedLoading (url); | |||
else if (cmd == "windowCloseRequest") owner.windowCloseRequest(); | |||
else if (cmd == "newWindowAttemptingToLoad") owner.newWindowAttemptingToLoad (url); | |||
else if (cmd == "pageLoadHadNetworkError") handlePageLoadHadNetworkError (params); | |||
threadBlocker.signal(); | |||
} | |||
void handlePageAboutToLoad (const String& url, const var& inputParams) | |||
{ | |||
int64 decision_id = inputParams.getProperty ("decision_id", var (0)); | |||
if (decision_id != 0) | |||
{ | |||
DynamicObject::Ptr params = new DynamicObject; | |||
params->setProperty ("decision_id", decision_id); | |||
params->setProperty ("allow", owner.pageAboutToLoad (url)); | |||
CommandReceiver::sendCommand (outChannel, "decision", var (params)); | |||
} | |||
} | |||
void handlePageLoadHadNetworkError (const var& params) | |||
{ | |||
String error = params.getProperty ("error", "Unknown error"); | |||
if (owner.pageLoadHadNetworkError (error)) | |||
goToURL (String ("data:text/plain,") + error, nullptr, nullptr); | |||
} | |||
void handleCommand (const String& cmd, const var& params) override | |||
{ | |||
threadBlocker.reset(); | |||
(new HandleOnMessageThread (this, cmd, params))->post(); | |||
// wait until the command has executed on the message thread | |||
// this ensures that Pimpl can never be deleted while the | |||
// message has not been executed yet | |||
threadBlocker.wait (-1); | |||
} | |||
void receiverHadError() override {} | |||
//============================================================================== | |||
struct HandleOnMessageThread : public CallbackMessage | |||
{ | |||
HandleOnMessageThread (Pimpl* pimpl, const String& cmdToUse, const var& params) | |||
: owner (pimpl), cmdToSend (cmdToUse), paramsToSend (params) | |||
{} | |||
void messageCallback() override | |||
{ | |||
owner->handleCommandOnMessageThread (cmdToSend, paramsToSend); | |||
} | |||
Pimpl* owner; | |||
String cmdToSend; | |||
var paramsToSend; | |||
}; | |||
private: | |||
WebBrowserComponent& owner; | |||
ScopedPointer<CommandReceiver> receiver; | |||
int childProcess = 0, inChannel = 0, outChannel = 0; | |||
int threadControl[2]; | |||
ScopedPointer<XEmbedComponent> xembed; | |||
WaitableEvent threadBlocker; | |||
}; | |||
//============================================================================== | |||
WebBrowserComponent::WebBrowserComponent (const bool unloadPageWhenBrowserIsHidden_) | |||
: browser (new Pimpl (*this)), | |||
blankPageShown (false), | |||
unloadPageWhenBrowserIsHidden (unloadPageWhenBrowserIsHidden_) | |||
{ | |||
setOpaque (true); | |||
browser->init(); | |||
} | |||
WebBrowserComponent::~WebBrowserComponent() | |||
{ | |||
if (browser != nullptr) | |||
{ | |||
delete browser; | |||
browser = nullptr; | |||
} | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::goToURL (const String& url, | |||
const StringArray* headers, | |||
const MemoryBlock* postData) | |||
{ | |||
lastURL = url; | |||
if (headers != nullptr) | |||
lastHeaders = *headers; | |||
else | |||
lastHeaders.clear(); | |||
if (postData != nullptr) | |||
lastPostData = *postData; | |||
else | |||
lastPostData.reset(); | |||
blankPageShown = false; | |||
browser->goToURL (url, headers, postData); | |||
} | |||
void WebBrowserComponent::stop() | |||
{ | |||
browser->stop(); | |||
} | |||
void WebBrowserComponent::goBack() | |||
{ | |||
lastURL.clear(); | |||
blankPageShown = false; | |||
browser->goBack(); | |||
} | |||
void WebBrowserComponent::goForward() | |||
{ | |||
lastURL.clear(); | |||
browser->goForward(); | |||
} | |||
void WebBrowserComponent::refresh() | |||
{ | |||
browser->refresh(); | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::paint (Graphics& g) | |||
{ | |||
g.fillAll (Colours::white); | |||
} | |||
void WebBrowserComponent::checkWindowAssociation() | |||
{ | |||
} | |||
void WebBrowserComponent::reloadLastURL() | |||
{ | |||
if (lastURL.isNotEmpty()) | |||
{ | |||
goToURL (lastURL, &lastHeaders, &lastPostData); | |||
lastURL.clear(); | |||
} | |||
} | |||
void WebBrowserComponent::parentHierarchyChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::resized() | |||
{ | |||
if (browser != nullptr) | |||
browser->resized(); | |||
} | |||
void WebBrowserComponent::visibilityChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::focusGained (FocusChangeType) | |||
{ | |||
} | |||
void WebBrowserComponent::clearCookies() | |||
{ | |||
// Currently not implemented on linux as WebBrowserComponent currently does not | |||
// store cookies on linux | |||
jassertfalse; | |||
} | |||
int juce_gtkWebkitMain (int argc, const char* argv[]) | |||
{ | |||
if (argc != 4) return -1; | |||
GtkChildProcess child (String (argv[2]).getIntValue(), | |||
String (argv[3]).getIntValue()); | |||
return child.entry(); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,685 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
bool juce_handleXEmbedEvent (ComponentPeer*, void*); | |||
Window juce_getCurrentFocusWindow (ComponentPeer*); | |||
//============================================================================== | |||
unsigned long juce_createKeyProxyWindow (ComponentPeer*); | |||
void juce_deleteKeyProxyWindow (ComponentPeer*); | |||
//============================================================================== | |||
class XEmbedComponent::Pimpl : private ComponentListener | |||
{ | |||
public: | |||
enum | |||
{ | |||
maxXEmbedVersionToSupport = 0 | |||
}; | |||
enum Flags | |||
{ | |||
XEMBED_MAPPED = (1<<0) | |||
}; | |||
enum | |||
{ | |||
XEMBED_EMBEDDED_NOTIFY = 0, | |||
XEMBED_WINDOW_ACTIVATE = 1, | |||
XEMBED_WINDOW_DEACTIVATE = 2, | |||
XEMBED_REQUEST_FOCUS = 3, | |||
XEMBED_FOCUS_IN = 4, | |||
XEMBED_FOCUS_OUT = 5, | |||
XEMBED_FOCUS_NEXT = 6, | |||
XEMBED_FOCUS_PREV = 7, | |||
XEMBED_MODALITY_ON = 10, | |||
XEMBED_MODALITY_OFF = 11, | |||
XEMBED_REGISTER_ACCELERATOR = 12, | |||
XEMBED_UNREGISTER_ACCELERATOR = 13, | |||
XEMBED_ACTIVATE_ACCELERATOR = 14 | |||
}; | |||
enum | |||
{ | |||
XEMBED_FOCUS_CURRENT = 0, | |||
XEMBED_FOCUS_FIRST = 1, | |||
XEMBED_FOCUS_LAST = 2 | |||
}; | |||
//============================================================================== | |||
class SharedKeyWindow : public ReferenceCountedObject | |||
{ | |||
public: | |||
typedef ReferenceCountedObjectPtr<SharedKeyWindow> Ptr; | |||
//============================================================================== | |||
Window getHandle() { return keyProxy; } | |||
static Window getCurrentFocusWindow (ComponentPeer* peerToLookFor) | |||
{ | |||
auto& keyWindows = getKeyWindows(); | |||
if (peerToLookFor != nullptr) | |||
if (auto* foundKeyWindow = keyWindows[peerToLookFor]) | |||
return foundKeyWindow->keyProxy; | |||
return {}; | |||
} | |||
static SharedKeyWindow::Ptr getKeyWindowForPeer (ComponentPeer* peerToLookFor) | |||
{ | |||
jassert (peerToLookFor != nullptr); | |||
auto& keyWindows = getKeyWindows(); | |||
auto foundKeyWindow = keyWindows[peerToLookFor]; | |||
if (foundKeyWindow == nullptr) | |||
{ | |||
foundKeyWindow = new SharedKeyWindow (peerToLookFor); | |||
keyWindows.set (peerToLookFor, foundKeyWindow); | |||
} | |||
return foundKeyWindow; | |||
} | |||
private: | |||
//============================================================================== | |||
friend struct ContainerDeletePolicy<SharedKeyWindow>; | |||
SharedKeyWindow (ComponentPeer* peerToUse) | |||
: keyPeer (peerToUse), | |||
keyProxy (juce_createKeyProxyWindow (keyPeer)) | |||
{} | |||
~SharedKeyWindow() | |||
{ | |||
juce_deleteKeyProxyWindow (keyPeer); | |||
auto& keyWindows = getKeyWindows(); | |||
keyWindows.remove (keyPeer); | |||
} | |||
ComponentPeer* keyPeer; | |||
Window keyProxy; | |||
static HashMap<ComponentPeer*, SharedKeyWindow*>& getKeyWindows() | |||
{ | |||
// store a weak reference to the shared key windows | |||
static HashMap<ComponentPeer*, SharedKeyWindow*> keyWindows; | |||
return keyWindows; | |||
} | |||
}; | |||
public: | |||
//============================================================================== | |||
Pimpl (XEmbedComponent& parent, Window x11Window, | |||
bool wantsKeyboardFocus, bool isClientInitiated, bool shouldAllowResize) | |||
: owner (parent), atoms (x11display.display), clientInitiated (isClientInitiated), | |||
wantsFocus (wantsKeyboardFocus), allowResize (shouldAllowResize) | |||
{ | |||
getWidgets().add (this); | |||
createHostWindow(); | |||
if (clientInitiated) | |||
setClient (x11Window, true); | |||
owner.setWantsKeyboardFocus (wantsFocus); | |||
owner.addComponentListener (this); | |||
} | |||
~Pimpl() | |||
{ | |||
owner.removeComponentListener (this); | |||
setClient (0, true); | |||
if (host != 0) | |||
{ | |||
auto dpy = getDisplay(); | |||
XDestroyWindow (dpy, host); | |||
XSync (dpy, false); | |||
const long mask = NoEventMask | KeyPressMask | KeyReleaseMask | |||
| EnterWindowMask | LeaveWindowMask | PointerMotionMask | |||
| KeymapStateMask | ExposureMask | StructureNotifyMask | |||
| FocusChangeMask; | |||
XEvent event; | |||
while (XCheckWindowEvent (dpy, host, mask, &event) == True) | |||
{} | |||
host = 0; | |||
} | |||
getWidgets().removeAllInstancesOf (this); | |||
} | |||
//============================================================================== | |||
void setClient (Window xembedClient, bool shouldReparent) | |||
{ | |||
removeClient(); | |||
if (xembedClient != 0) | |||
{ | |||
auto dpy = getDisplay(); | |||
client = xembedClient; | |||
// if the client has initiated the component then keep the clients size | |||
// otherwise the client should use the host's window' size | |||
if (clientInitiated) | |||
{ | |||
configureNotify(); | |||
} | |||
else | |||
{ | |||
auto newBounds = getX11BoundsFromJuce(); | |||
XResizeWindow (dpy, client, static_cast<unsigned int> (newBounds.getWidth()), | |||
static_cast<unsigned int> (newBounds.getHeight())); | |||
} | |||
XSelectInput (dpy, client, StructureNotifyMask | PropertyChangeMask | FocusChangeMask); | |||
getXEmbedMappedFlag(); | |||
if (shouldReparent) | |||
XReparentWindow (dpy, client, host, 0, 0); | |||
if (supportsXembed) | |||
sendXEmbedEvent (CurrentTime, XEMBED_EMBEDDED_NOTIFY, 0, (long) host, xembedVersion); | |||
updateMapping(); | |||
} | |||
} | |||
void focusGained (FocusChangeType changeType) | |||
{ | |||
if (client != 0 && supportsXembed && wantsFocus) | |||
{ | |||
updateKeyFocus(); | |||
sendXEmbedEvent (CurrentTime, XEMBED_FOCUS_IN, | |||
(changeType == focusChangedByTabKey ? XEMBED_FOCUS_FIRST : XEMBED_FOCUS_CURRENT)); | |||
} | |||
} | |||
void focusLost (FocusChangeType) | |||
{ | |||
if (client != 0 && supportsXembed && wantsFocus) | |||
{ | |||
sendXEmbedEvent (CurrentTime, XEMBED_FOCUS_OUT); | |||
updateKeyFocus(); | |||
} | |||
} | |||
void broughtToFront() | |||
{ | |||
if (client != 0 && supportsXembed) | |||
sendXEmbedEvent (CurrentTime, XEMBED_WINDOW_ACTIVATE); | |||
} | |||
unsigned long getHostWindowID() | |||
{ | |||
// You are using the client initiated version of the protocol. You cannot | |||
// retrieve the window id of the host. Please read the documentation for | |||
// the XEmebedComponent class. | |||
jassert (! clientInitiated); | |||
return host; | |||
} | |||
private: | |||
//============================================================================== | |||
XEmbedComponent& owner; | |||
Window client = 0, host = 0; | |||
ScopedXDisplay x11display; | |||
Atoms atoms; | |||
bool clientInitiated; | |||
bool wantsFocus = false; | |||
bool allowResize = false; | |||
bool supportsXembed = false; | |||
bool hasBeenMapped = false; | |||
int xembedVersion = maxXEmbedVersionToSupport; | |||
ComponentPeer* lastPeer = nullptr; | |||
SharedKeyWindow::Ptr keyWindow; | |||
//============================================================================== | |||
void componentParentHierarchyChanged (Component&) override { peerChanged (owner.getPeer()); } | |||
void componentMovedOrResized (Component&, bool, bool) override | |||
{ | |||
if (host != 0 && lastPeer != nullptr) | |||
{ | |||
auto dpy = getDisplay(); | |||
auto newBounds = getX11BoundsFromJuce(); | |||
XWindowAttributes attr; | |||
if (XGetWindowAttributes (dpy, host, &attr)) | |||
{ | |||
Rectangle<int> currentBounds (attr.x, attr.y, attr.width, attr.height); | |||
if (currentBounds != newBounds) | |||
{ | |||
XMoveResizeWindow (dpy, host, newBounds.getX(), newBounds.getY(), | |||
static_cast<unsigned int> (newBounds.getWidth()), | |||
static_cast<unsigned int> (newBounds.getHeight())); | |||
} | |||
} | |||
if (client != 0 && XGetWindowAttributes (dpy, client, &attr)) | |||
{ | |||
Rectangle<int> currentBounds (attr.x, attr.y, attr.width, attr.height); | |||
if ((currentBounds.getWidth() != newBounds.getWidth() | |||
|| currentBounds.getHeight() != newBounds.getHeight())) | |||
{ | |||
XMoveResizeWindow (dpy, client, 0, 0, | |||
static_cast<unsigned int> (newBounds.getWidth()), | |||
static_cast<unsigned int> (newBounds.getHeight())); | |||
} | |||
} | |||
} | |||
} | |||
//============================================================================== | |||
void createHostWindow() | |||
{ | |||
auto dpy = getDisplay(); | |||
int defaultScreen = XDefaultScreen (dpy); | |||
Window root = RootWindow (dpy, defaultScreen); | |||
XSetWindowAttributes swa; | |||
swa.border_pixel = 0; | |||
swa.background_pixmap = None; | |||
swa.override_redirect = True; | |||
swa.event_mask = SubstructureNotifyMask | StructureNotifyMask | FocusChangeMask; | |||
host = XCreateWindow (dpy, root, 0, 0, 1, 1, 0, CopyFromParent, | |||
InputOutput, CopyFromParent, | |||
CWEventMask | CWBorderPixel | CWBackPixmap | CWOverrideRedirect, | |||
&swa); | |||
} | |||
void removeClient() | |||
{ | |||
if (client != 0) | |||
{ | |||
auto dpy = getDisplay(); | |||
XSelectInput (dpy, client, 0); | |||
keyWindow = nullptr; | |||
int defaultScreen = XDefaultScreen (dpy); | |||
Window root = RootWindow (dpy, defaultScreen); | |||
if (hasBeenMapped) | |||
{ | |||
XUnmapWindow (dpy, client); | |||
hasBeenMapped = false; | |||
} | |||
XReparentWindow (dpy, client, root, 0, 0); | |||
client = 0; | |||
} | |||
} | |||
void updateMapping() | |||
{ | |||
if (client != 0) | |||
{ | |||
const bool shouldBeMapped = getXEmbedMappedFlag(); | |||
if (shouldBeMapped != hasBeenMapped) | |||
{ | |||
hasBeenMapped = shouldBeMapped; | |||
if (shouldBeMapped) | |||
XMapWindow (getDisplay(), client); | |||
else | |||
XUnmapWindow (getDisplay(), client); | |||
} | |||
} | |||
} | |||
Window getParentX11Window() | |||
{ | |||
if (auto peer = owner.getPeer()) | |||
return reinterpret_cast<Window> (peer->getNativeHandle()); | |||
return {}; | |||
} | |||
Display* getDisplay() { return reinterpret_cast<Display*> (x11display.display); } | |||
//============================================================================== | |||
bool getXEmbedMappedFlag() | |||
{ | |||
GetXProperty embedInfo (x11display.display, client, atoms.XembedInfo, 0, 2, false, atoms.XembedInfo); | |||
if (embedInfo.success && embedInfo.actualFormat == 32 | |||
&& embedInfo.numItems >= 2 && embedInfo.data != nullptr) | |||
{ | |||
auto* buffer = (long*) embedInfo.data; | |||
supportsXembed = true; | |||
xembedVersion = jmin ((int) maxXEmbedVersionToSupport, (int) buffer[0]); | |||
return ((buffer[1] & XEMBED_MAPPED) != 0); | |||
} | |||
else | |||
{ | |||
supportsXembed = false; | |||
xembedVersion = maxXEmbedVersionToSupport; | |||
} | |||
return true; | |||
} | |||
//============================================================================== | |||
void propertyChanged (const Atom& a) | |||
{ | |||
if (a == atoms.XembedInfo) | |||
updateMapping(); | |||
} | |||
void configureNotify() | |||
{ | |||
XWindowAttributes attr; | |||
auto dpy = getDisplay(); | |||
if (XGetWindowAttributes (dpy, client, &attr)) | |||
{ | |||
XWindowAttributes hostAttr; | |||
if (XGetWindowAttributes (dpy, host, &hostAttr)) | |||
if (attr.width != hostAttr.width || attr.height != hostAttr.height) | |||
XResizeWindow (dpy, host, (unsigned int) attr.width, (unsigned int) attr.height); | |||
// as the client window is not on any screen yet, we need to guess | |||
// on which screen it might appear to get a scaling factor :-( | |||
auto& displays = Desktop::getInstance().getDisplays(); | |||
auto* peer = owner.getPeer(); | |||
const double scale = (peer != nullptr ? displays.getDisplayContaining (peer->getBounds().getCentre()) | |||
: displays.getMainDisplay()).scale; | |||
Point<int> topLeftInPeer | |||
= (peer != nullptr ? peer->getComponent().getLocalPoint (&owner, Point<int> (0, 0)) | |||
: owner.getBounds().getTopLeft()); | |||
Rectangle<int> newBounds (topLeftInPeer.getX(), topLeftInPeer.getY(), | |||
static_cast<int> (static_cast<double> (attr.width) / scale), | |||
static_cast<int> (static_cast<double> (attr.height) / scale)); | |||
if (peer != nullptr) | |||
newBounds = owner.getLocalArea (&peer->getComponent(), newBounds); | |||
jassert (newBounds.getX() == 0 && newBounds.getY() == 0); | |||
if (newBounds != owner.getLocalBounds()) | |||
owner.setSize (newBounds.getWidth(), newBounds.getHeight()); | |||
} | |||
} | |||
void peerChanged (ComponentPeer* newPeer) | |||
{ | |||
if (newPeer != lastPeer) | |||
{ | |||
if (lastPeer != nullptr) | |||
keyWindow = nullptr; | |||
auto dpy = getDisplay(); | |||
Window rootWindow = RootWindow (dpy, DefaultScreen (dpy)); | |||
Rectangle<int> newBounds = getX11BoundsFromJuce(); | |||
if (newPeer == nullptr) | |||
XUnmapWindow (dpy, host); | |||
Window newParent = (newPeer != nullptr ? getParentX11Window() : rootWindow); | |||
XReparentWindow (dpy, host, newParent, newBounds.getX(), newBounds.getY()); | |||
lastPeer = newPeer; | |||
if (newPeer != nullptr) | |||
{ | |||
if (wantsFocus) | |||
{ | |||
keyWindow = SharedKeyWindow::getKeyWindowForPeer (newPeer); | |||
updateKeyFocus(); | |||
} | |||
componentMovedOrResized (owner, true, true); | |||
XMapWindow (dpy, host); | |||
broughtToFront(); | |||
} | |||
} | |||
} | |||
void updateKeyFocus() | |||
{ | |||
if (lastPeer != nullptr && lastPeer->isFocused()) | |||
XSetInputFocus (getDisplay(), getCurrentFocusWindow (lastPeer), RevertToParent, CurrentTime); | |||
} | |||
//============================================================================== | |||
void handleXembedCmd (const ::Time& /*xTime*/, long opcode, long /*detail*/, long /*data1*/, long /*data2*/) | |||
{ | |||
switch (opcode) | |||
{ | |||
case XEMBED_REQUEST_FOCUS: | |||
if (wantsFocus) | |||
owner.grabKeyboardFocus(); | |||
break; | |||
case XEMBED_FOCUS_NEXT: | |||
if (wantsFocus) | |||
owner.moveKeyboardFocusToSibling (true); | |||
break; | |||
case XEMBED_FOCUS_PREV: | |||
if (wantsFocus) | |||
owner.moveKeyboardFocusToSibling (false); | |||
break; | |||
} | |||
} | |||
bool handleX11Event (const XEvent& e) | |||
{ | |||
if (e.xany.window == client && client != 0) | |||
{ | |||
switch (e.type) | |||
{ | |||
case PropertyNotify: | |||
propertyChanged (e.xproperty.atom); | |||
return true; | |||
case ConfigureNotify: | |||
if (allowResize) | |||
configureNotify(); | |||
else | |||
MessageManager::callAsync([this] () {componentMovedOrResized (owner, true, true);}); | |||
return true; | |||
} | |||
} | |||
else if (e.xany.window == host && host != 0) | |||
{ | |||
switch (e.type) | |||
{ | |||
case ReparentNotify: | |||
if (e.xreparent.parent == host && e.xreparent.window != client) | |||
{ | |||
setClient (e.xreparent.window, false); | |||
return true; | |||
} | |||
break; | |||
case CreateNotify: | |||
if (e.xcreatewindow.parent != e.xcreatewindow.window && e.xcreatewindow.parent == host && e.xcreatewindow.window != client) | |||
{ | |||
setClient (e.xcreatewindow.window, false); | |||
return true; | |||
} | |||
break; | |||
case GravityNotify: | |||
componentMovedOrResized (owner, true, true); | |||
return true; | |||
case ClientMessage: | |||
if (e.xclient.message_type == atoms.XembedMsgType && e.xclient.format == 32) | |||
{ | |||
handleXembedCmd ((::Time) e.xclient.data.l[0], e.xclient.data.l[1], | |||
e.xclient.data.l[2], e.xclient.data.l[3], | |||
e.xclient.data.l[4]); | |||
return true; | |||
} | |||
break; | |||
} | |||
} | |||
return false; | |||
} | |||
void sendXEmbedEvent (const ::Time& xTime, long opcode, | |||
long opcodeMinor = 0, long data1 = 0, long data2 = 0) | |||
{ | |||
XClientMessageEvent msg; | |||
auto dpy = getDisplay(); | |||
::memset (&msg, 0, sizeof (XClientMessageEvent)); | |||
msg.window = client; | |||
msg.type = ClientMessage; | |||
msg.message_type = atoms.XembedMsgType; | |||
msg.format = 32; | |||
msg.data.l[0] = (long) xTime; | |||
msg.data.l[1] = opcode; | |||
msg.data.l[2] = opcodeMinor; | |||
msg.data.l[3] = data1; | |||
msg.data.l[4] = data2; | |||
XSendEvent (dpy, client, False, NoEventMask, (XEvent*) &msg); | |||
XSync (dpy, False); | |||
} | |||
Rectangle<int> getX11BoundsFromJuce() | |||
{ | |||
if (auto* peer = owner.getPeer()) | |||
{ | |||
auto r = peer->getComponent().getLocalArea (&owner, owner.getLocalBounds()); | |||
auto scale = Desktop::getInstance().getDisplays().getDisplayContaining (peer->localToGlobal (r.getCentre())).scale; | |||
return r * scale; | |||
} | |||
return owner.getLocalBounds(); | |||
} | |||
//============================================================================== | |||
friend bool juce::juce_handleXEmbedEvent (ComponentPeer*, void*); | |||
friend unsigned long juce::juce_getCurrentFocusWindow (ComponentPeer*); | |||
static Array<Pimpl*>& getWidgets() | |||
{ | |||
static Array<Pimpl*> i; | |||
return i; | |||
} | |||
static bool dispatchX11Event (ComponentPeer* p, const XEvent* eventArg) | |||
{ | |||
if (eventArg != nullptr) | |||
{ | |||
auto& e = *eventArg; | |||
if (auto w = e.xany.window) | |||
for (auto* widget : getWidgets()) | |||
if (w == widget->host || w == widget->client) | |||
return widget->handleX11Event (e); | |||
} | |||
else | |||
{ | |||
for (auto* widget : getWidgets()) | |||
if (widget->owner.getPeer() == p) | |||
widget->peerChanged (nullptr); | |||
} | |||
return false; | |||
} | |||
static Window getCurrentFocusWindow (ComponentPeer* p) | |||
{ | |||
if (p != nullptr) | |||
{ | |||
for (auto* widget : getWidgets()) | |||
if (widget->owner.getPeer() == p && widget->owner.hasKeyboardFocus (false)) | |||
return widget->client; | |||
} | |||
return SharedKeyWindow::getCurrentFocusWindow (p); | |||
} | |||
}; | |||
//============================================================================== | |||
XEmbedComponent::XEmbedComponent (bool wantsKeyboardFocus, bool allowForeignWidgetToResizeComponent) | |||
: pimpl (new Pimpl (*this, 0, wantsKeyboardFocus, false, allowForeignWidgetToResizeComponent)) | |||
{ | |||
setOpaque (true); | |||
} | |||
XEmbedComponent::XEmbedComponent (unsigned long wID, bool wantsKeyboardFocus, bool allowForeignWidgetToResizeComponent) | |||
: pimpl (new Pimpl (*this, wID, wantsKeyboardFocus, true, allowForeignWidgetToResizeComponent)) | |||
{ | |||
setOpaque (true); | |||
} | |||
XEmbedComponent::~XEmbedComponent() {} | |||
void XEmbedComponent::paint (Graphics& g) | |||
{ | |||
g.fillAll (Colours::lightgrey); | |||
} | |||
void XEmbedComponent::focusGained (FocusChangeType changeType) { pimpl->focusGained (changeType); } | |||
void XEmbedComponent::focusLost (FocusChangeType changeType) { pimpl->focusLost (changeType); } | |||
void XEmbedComponent::broughtToFront() { pimpl->broughtToFront(); } | |||
unsigned long XEmbedComponent::getHostWindowID() { return pimpl->getHostWindowID(); } | |||
//============================================================================== | |||
bool juce_handleXEmbedEvent (ComponentPeer* p, void* e) | |||
{ | |||
return XEmbedComponent::Pimpl::dispatchX11Event (p, reinterpret_cast<const XEvent*> (e)); | |||
} | |||
unsigned long juce_getCurrentFocusWindow (ComponentPeer* peer) | |||
{ | |||
return (unsigned long) XEmbedComponent::Pimpl::getCurrentFocusWindow (peer); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,269 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
AppleRemoteDevice::AppleRemoteDevice() | |||
: device (nullptr), | |||
queue (nullptr), | |||
remoteId (0) | |||
{ | |||
} | |||
AppleRemoteDevice::~AppleRemoteDevice() | |||
{ | |||
stop(); | |||
} | |||
namespace | |||
{ | |||
io_object_t getAppleRemoteDevice() | |||
{ | |||
CFMutableDictionaryRef dict = IOServiceMatching ("AppleIRController"); | |||
io_iterator_t iter = 0; | |||
io_object_t iod = 0; | |||
if (IOServiceGetMatchingServices (kIOMasterPortDefault, dict, &iter) == kIOReturnSuccess | |||
&& iter != 0) | |||
{ | |||
iod = IOIteratorNext (iter); | |||
} | |||
IOObjectRelease (iter); | |||
return iod; | |||
} | |||
bool createAppleRemoteInterface (io_object_t iod, void** device) | |||
{ | |||
jassert (*device == nullptr); | |||
io_name_t classname; | |||
if (IOObjectGetClass (iod, classname) == kIOReturnSuccess) | |||
{ | |||
IOCFPlugInInterface** cfPlugInInterface = nullptr; | |||
SInt32 score = 0; | |||
if (IOCreatePlugInInterfaceForService (iod, | |||
kIOHIDDeviceUserClientTypeID, | |||
kIOCFPlugInInterfaceID, | |||
&cfPlugInInterface, | |||
&score) == kIOReturnSuccess) | |||
{ | |||
HRESULT hr = (*cfPlugInInterface)->QueryInterface (cfPlugInInterface, | |||
CFUUIDGetUUIDBytes (kIOHIDDeviceInterfaceID), | |||
device); | |||
ignoreUnused (hr); | |||
(*cfPlugInInterface)->Release (cfPlugInInterface); | |||
} | |||
} | |||
return *device != nullptr; | |||
} | |||
void appleRemoteQueueCallback (void* const target, const IOReturn result, void*, void*) | |||
{ | |||
if (result == kIOReturnSuccess) | |||
((AppleRemoteDevice*) target)->handleCallbackInternal(); | |||
} | |||
} | |||
bool AppleRemoteDevice::start (const bool inExclusiveMode) | |||
{ | |||
if (queue != nullptr) | |||
return true; | |||
stop(); | |||
bool result = false; | |||
io_object_t iod = getAppleRemoteDevice(); | |||
if (iod != 0) | |||
{ | |||
if (createAppleRemoteInterface (iod, &device) && open (inExclusiveMode)) | |||
result = true; | |||
else | |||
stop(); | |||
IOObjectRelease (iod); | |||
} | |||
return result; | |||
} | |||
void AppleRemoteDevice::stop() | |||
{ | |||
if (queue != nullptr) | |||
{ | |||
(*(IOHIDQueueInterface**) queue)->stop ((IOHIDQueueInterface**) queue); | |||
(*(IOHIDQueueInterface**) queue)->dispose ((IOHIDQueueInterface**) queue); | |||
(*(IOHIDQueueInterface**) queue)->Release ((IOHIDQueueInterface**) queue); | |||
queue = nullptr; | |||
} | |||
if (device != nullptr) | |||
{ | |||
(*(IOHIDDeviceInterface**) device)->close ((IOHIDDeviceInterface**) device); | |||
(*(IOHIDDeviceInterface**) device)->Release ((IOHIDDeviceInterface**) device); | |||
device = nullptr; | |||
} | |||
} | |||
bool AppleRemoteDevice::isActive() const | |||
{ | |||
return queue != nullptr; | |||
} | |||
bool AppleRemoteDevice::open (const bool openInExclusiveMode) | |||
{ | |||
Array <int> cookies; | |||
CFArrayRef elements; | |||
IOHIDDeviceInterface122** const device122 = (IOHIDDeviceInterface122**) device; | |||
if ((*device122)->copyMatchingElements (device122, 0, &elements) != kIOReturnSuccess) | |||
return false; | |||
for (int i = 0; i < CFArrayGetCount (elements); ++i) | |||
{ | |||
CFDictionaryRef element = (CFDictionaryRef) CFArrayGetValueAtIndex (elements, i); | |||
// get the cookie | |||
CFTypeRef object = CFDictionaryGetValue (element, CFSTR (kIOHIDElementCookieKey)); | |||
if (object == 0 || CFGetTypeID (object) != CFNumberGetTypeID()) | |||
continue; | |||
long number; | |||
if (! CFNumberGetValue ((CFNumberRef) object, kCFNumberLongType, &number)) | |||
continue; | |||
cookies.add ((int) number); | |||
} | |||
CFRelease (elements); | |||
if ((*(IOHIDDeviceInterface**) device) | |||
->open ((IOHIDDeviceInterface**) device, | |||
openInExclusiveMode ? kIOHIDOptionsTypeSeizeDevice | |||
: kIOHIDOptionsTypeNone) == KERN_SUCCESS) | |||
{ | |||
queue = (*(IOHIDDeviceInterface**) device)->allocQueue ((IOHIDDeviceInterface**) device); | |||
if (queue != 0) | |||
{ | |||
(*(IOHIDQueueInterface**) queue)->create ((IOHIDQueueInterface**) queue, 0, 12); | |||
for (int i = 0; i < cookies.size(); ++i) | |||
{ | |||
IOHIDElementCookie cookie = (IOHIDElementCookie) cookies.getUnchecked(i); | |||
(*(IOHIDQueueInterface**) queue)->addElement ((IOHIDQueueInterface**) queue, cookie, 0); | |||
} | |||
CFRunLoopSourceRef eventSource; | |||
if ((*(IOHIDQueueInterface**) queue) | |||
->createAsyncEventSource ((IOHIDQueueInterface**) queue, &eventSource) == KERN_SUCCESS) | |||
{ | |||
if ((*(IOHIDQueueInterface**) queue)->setEventCallout ((IOHIDQueueInterface**) queue, | |||
appleRemoteQueueCallback, this, 0) == KERN_SUCCESS) | |||
{ | |||
CFRunLoopAddSource (CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode); | |||
(*(IOHIDQueueInterface**) queue)->start ((IOHIDQueueInterface**) queue); | |||
return true; | |||
} | |||
} | |||
} | |||
} | |||
return false; | |||
} | |||
void AppleRemoteDevice::handleCallbackInternal() | |||
{ | |||
int totalValues = 0; | |||
AbsoluteTime nullTime = { 0, 0 }; | |||
char cookies [12]; | |||
int numCookies = 0; | |||
while (numCookies < numElementsInArray (cookies)) | |||
{ | |||
IOHIDEventStruct e; | |||
if ((*(IOHIDQueueInterface**) queue)->getNextEvent ((IOHIDQueueInterface**) queue, &e, nullTime, 0) != kIOReturnSuccess) | |||
break; | |||
if ((int) e.elementCookie == 19) | |||
{ | |||
remoteId = e.value; | |||
buttonPressed (switched, false); | |||
} | |||
else | |||
{ | |||
totalValues += e.value; | |||
cookies [numCookies++] = (char) (pointer_sized_int) e.elementCookie; | |||
} | |||
} | |||
cookies [numCookies++] = 0; | |||
static const char buttonPatterns[] = | |||
{ | |||
0x1f, 0x14, 0x12, 0x1f, 0x14, 0x12, 0, | |||
0x1f, 0x15, 0x12, 0x1f, 0x15, 0x12, 0, | |||
0x1f, 0x1d, 0x1c, 0x12, 0, | |||
0x1f, 0x1e, 0x1c, 0x12, 0, | |||
0x1f, 0x16, 0x12, 0x1f, 0x16, 0x12, 0, | |||
0x1f, 0x17, 0x12, 0x1f, 0x17, 0x12, 0, | |||
0x1f, 0x12, 0x04, 0x02, 0, | |||
0x1f, 0x12, 0x03, 0x02, 0, | |||
0x1f, 0x12, 0x1f, 0x12, 0, | |||
0x23, 0x1f, 0x12, 0x23, 0x1f, 0x12, 0, | |||
19, 0 | |||
}; | |||
int buttonNum = (int) menuButton; | |||
int i = 0; | |||
while (i < numElementsInArray (buttonPatterns)) | |||
{ | |||
if (strcmp (cookies, buttonPatterns + i) == 0) | |||
{ | |||
buttonPressed ((ButtonType) buttonNum, totalValues > 0); | |||
break; | |||
} | |||
i += (int) strlen (buttonPatterns + i) + 1; | |||
++buttonNum; | |||
} | |||
} | |||
} // namespace juce |
@@ -0,0 +1,345 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
Creates a floating carbon window that can be used to hold a carbon UI. | |||
This is a handy class that's designed to be inlined where needed, e.g. | |||
in the audio plugin hosting code. | |||
*/ | |||
class CarbonViewWrapperComponent : public Component, | |||
public ComponentMovementWatcher, | |||
public Timer | |||
{ | |||
public: | |||
CarbonViewWrapperComponent() | |||
: ComponentMovementWatcher (this), | |||
carbonWindow (nil), | |||
keepPluginWindowWhenHidden (false), | |||
wrapperWindow (nil), | |||
embeddedView (0), | |||
recursiveResize (false), | |||
repaintChildOnCreation (true) | |||
{ | |||
} | |||
~CarbonViewWrapperComponent() | |||
{ | |||
jassert (embeddedView == 0); // must call deleteWindow() in the subclass's destructor! | |||
} | |||
virtual HIViewRef attachView (WindowRef windowRef, HIViewRef rootView) = 0; | |||
virtual void removeView (HIViewRef embeddedView) = 0; | |||
virtual void handleMouseDown (int, int) {} | |||
virtual void handlePaint() {} | |||
virtual bool getEmbeddedViewSize (int& w, int& h) | |||
{ | |||
if (embeddedView == 0) | |||
return false; | |||
HIRect bounds; | |||
HIViewGetBounds (embeddedView, &bounds); | |||
w = jmax (1, roundToInt (bounds.size.width)); | |||
h = jmax (1, roundToInt (bounds.size.height)); | |||
return true; | |||
} | |||
void createWindow() | |||
{ | |||
if (wrapperWindow == nil) | |||
{ | |||
Rect r; | |||
r.left = (short) getScreenX(); | |||
r.top = (short) getScreenY(); | |||
r.right = (short) (r.left + getWidth()); | |||
r.bottom = (short) (r.top + getHeight()); | |||
CreateNewWindow (kDocumentWindowClass, | |||
(WindowAttributes) (kWindowStandardHandlerAttribute | kWindowCompositingAttribute | |||
| kWindowNoShadowAttribute | kWindowNoTitleBarAttribute), | |||
&r, &wrapperWindow); | |||
jassert (wrapperWindow != 0); | |||
if (wrapperWindow == 0) | |||
return; | |||
carbonWindow = [[NSWindow alloc] initWithWindowRef: wrapperWindow]; | |||
[getOwnerWindow() addChildWindow: carbonWindow | |||
ordered: NSWindowAbove]; | |||
embeddedView = attachView (wrapperWindow, HIViewGetRoot (wrapperWindow)); | |||
// Check for the plugin creating its own floating window, and if there is one, | |||
// we need to reparent it to make it visible.. | |||
if (carbonWindow.childWindows.count > 0) | |||
if (NSWindow* floatingChildWindow = [[carbonWindow childWindows] objectAtIndex: 0]) | |||
[getOwnerWindow() addChildWindow: floatingChildWindow | |||
ordered: NSWindowAbove]; | |||
EventTypeSpec windowEventTypes[] = | |||
{ | |||
{ kEventClassWindow, kEventWindowGetClickActivation }, | |||
{ kEventClassWindow, kEventWindowHandleDeactivate }, | |||
{ kEventClassWindow, kEventWindowBoundsChanging }, | |||
{ kEventClassMouse, kEventMouseDown }, | |||
{ kEventClassMouse, kEventMouseMoved }, | |||
{ kEventClassMouse, kEventMouseDragged }, | |||
{ kEventClassMouse, kEventMouseUp }, | |||
{ kEventClassWindow, kEventWindowDrawContent }, | |||
{ kEventClassWindow, kEventWindowShown }, | |||
{ kEventClassWindow, kEventWindowHidden } | |||
}; | |||
EventHandlerUPP upp = NewEventHandlerUPP (carbonEventCallback); | |||
InstallWindowEventHandler (wrapperWindow, upp, | |||
sizeof (windowEventTypes) / sizeof (EventTypeSpec), | |||
windowEventTypes, this, &eventHandlerRef); | |||
setOurSizeToEmbeddedViewSize(); | |||
setEmbeddedWindowToOurSize(); | |||
creationTime = Time::getCurrentTime(); | |||
} | |||
} | |||
void deleteWindow() | |||
{ | |||
removeView (embeddedView); | |||
embeddedView = 0; | |||
if (wrapperWindow != nil) | |||
{ | |||
NSWindow* ownerWindow = getOwnerWindow(); | |||
if ([[ownerWindow childWindows] count] > 0) | |||
{ | |||
[ownerWindow removeChildWindow: carbonWindow]; | |||
[carbonWindow close]; | |||
} | |||
RemoveEventHandler (eventHandlerRef); | |||
DisposeWindow (wrapperWindow); | |||
wrapperWindow = nil; | |||
} | |||
} | |||
//============================================================================== | |||
void setOurSizeToEmbeddedViewSize() | |||
{ | |||
int w, h; | |||
if (getEmbeddedViewSize (w, h)) | |||
{ | |||
if (w != getWidth() || h != getHeight()) | |||
{ | |||
startTimer (50); | |||
setSize (w, h); | |||
if (Component* p = getParentComponent()) | |||
p->setSize (w, h); | |||
} | |||
else | |||
{ | |||
startTimer (jlimit (50, 500, getTimerInterval() + 20)); | |||
} | |||
} | |||
else | |||
{ | |||
stopTimer(); | |||
} | |||
} | |||
void setEmbeddedWindowToOurSize() | |||
{ | |||
if (! recursiveResize) | |||
{ | |||
recursiveResize = true; | |||
if (embeddedView != 0) | |||
{ | |||
HIRect r; | |||
r.origin.x = 0; | |||
r.origin.y = 0; | |||
r.size.width = (float) getWidth(); | |||
r.size.height = (float) getHeight(); | |||
HIViewSetFrame (embeddedView, &r); | |||
} | |||
if (wrapperWindow != nil) | |||
{ | |||
jassert (getTopLevelComponent()->getDesktopScaleFactor() == 1.0f); | |||
Rectangle<int> screenBounds (getScreenBounds() * Desktop::getInstance().getGlobalScaleFactor()); | |||
Rect wr; | |||
wr.left = (short) screenBounds.getX(); | |||
wr.top = (short) screenBounds.getY(); | |||
wr.right = (short) screenBounds.getRight(); | |||
wr.bottom = (short) screenBounds.getBottom(); | |||
SetWindowBounds (wrapperWindow, kWindowContentRgn, &wr); | |||
// This group stuff is mainly a workaround for Mackie plugins like FinalMix.. | |||
WindowGroupRef group = GetWindowGroup (wrapperWindow); | |||
WindowRef attachedWindow; | |||
if (GetIndexedWindow (group, 2, kWindowGroupContentsReturnWindows, &attachedWindow) == noErr) | |||
{ | |||
SelectWindow (attachedWindow); | |||
ActivateWindow (attachedWindow, TRUE); | |||
HideWindow (wrapperWindow); | |||
} | |||
ShowWindow (wrapperWindow); | |||
} | |||
recursiveResize = false; | |||
} | |||
} | |||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override | |||
{ | |||
setEmbeddedWindowToOurSize(); | |||
} | |||
// (overridden to intercept movements of the top-level window) | |||
void componentMovedOrResized (Component& component, bool wasMoved, bool wasResized) override | |||
{ | |||
ComponentMovementWatcher::componentMovedOrResized (component, wasMoved, wasResized); | |||
if (&component == getTopLevelComponent()) | |||
setEmbeddedWindowToOurSize(); | |||
} | |||
void componentPeerChanged() override | |||
{ | |||
deleteWindow(); | |||
createWindow(); | |||
} | |||
void componentVisibilityChanged() override | |||
{ | |||
if (isShowing()) | |||
createWindow(); | |||
else if (! keepPluginWindowWhenHidden) | |||
deleteWindow(); | |||
setEmbeddedWindowToOurSize(); | |||
} | |||
static void recursiveHIViewRepaint (HIViewRef view) | |||
{ | |||
HIViewSetNeedsDisplay (view, true); | |||
HIViewRef child = HIViewGetFirstSubview (view); | |||
while (child != 0) | |||
{ | |||
recursiveHIViewRepaint (child); | |||
child = HIViewGetNextView (child); | |||
} | |||
} | |||
void timerCallback() override | |||
{ | |||
if (isShowing()) | |||
{ | |||
setOurSizeToEmbeddedViewSize(); | |||
// To avoid strange overpainting problems when the UI is first opened, we'll | |||
// repaint it a few times during the first second that it's on-screen.. | |||
if (repaintChildOnCreation && (Time::getCurrentTime() - creationTime).inMilliseconds() < 1000) | |||
recursiveHIViewRepaint (HIViewGetRoot (wrapperWindow)); | |||
} | |||
} | |||
void setRepaintsChildHIViewWhenCreated (bool b) noexcept | |||
{ | |||
repaintChildOnCreation = b; | |||
} | |||
OSStatus carbonEventHandler (EventHandlerCallRef /*nextHandlerRef*/, EventRef event) | |||
{ | |||
switch (GetEventKind (event)) | |||
{ | |||
case kEventWindowHandleDeactivate: | |||
ActivateWindow (wrapperWindow, TRUE); | |||
return noErr; | |||
case kEventWindowGetClickActivation: | |||
{ | |||
getTopLevelComponent()->toFront (false); | |||
[carbonWindow makeKeyAndOrderFront: nil]; | |||
ClickActivationResult howToHandleClick = kActivateAndHandleClick; | |||
SetEventParameter (event, kEventParamClickActivation, typeClickActivationResult, | |||
sizeof (ClickActivationResult), &howToHandleClick); | |||
if (embeddedView != 0) | |||
HIViewSetNeedsDisplay (embeddedView, true); | |||
return noErr; | |||
} | |||
} | |||
return eventNotHandledErr; | |||
} | |||
static pascal OSStatus carbonEventCallback (EventHandlerCallRef nextHandlerRef, EventRef event, void* userData) | |||
{ | |||
return ((CarbonViewWrapperComponent*) userData)->carbonEventHandler (nextHandlerRef, event); | |||
} | |||
NSWindow* carbonWindow; | |||
bool keepPluginWindowWhenHidden; | |||
protected: | |||
WindowRef wrapperWindow; | |||
HIViewRef embeddedView; | |||
bool recursiveResize, repaintChildOnCreation; | |||
Time creationTime; | |||
EventHandlerRef eventHandlerRef; | |||
NSWindow* getOwnerWindow() const { return [((NSView*) getWindowHandle()) window]; } | |||
}; | |||
//============================================================================== | |||
// Non-public utility function that hosts can use if they need to get hold of the | |||
// internals of a carbon wrapper window.. | |||
void* getCarbonWindow (Component* possibleCarbonComponent) | |||
{ | |||
if (CarbonViewWrapperComponent* cv = dynamic_cast<CarbonViewWrapperComponent*> (possibleCarbonComponent)) | |||
return cv->carbonWindow; | |||
return nullptr; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,247 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
struct NSViewResizeWatcher | |||
{ | |||
NSViewResizeWatcher() : callback (nil) {} | |||
virtual ~NSViewResizeWatcher() | |||
{ | |||
// must call detachViewWatcher() first | |||
jassert (callback == nil); | |||
} | |||
void attachViewWatcher (NSView* view) | |||
{ | |||
static ViewFrameChangeCallbackClass cls; | |||
callback = [cls.createInstance() init]; | |||
ViewFrameChangeCallbackClass::setTarget (callback, this); | |||
[[NSNotificationCenter defaultCenter] addObserver: callback | |||
selector: @selector (frameChanged:) | |||
name: NSViewFrameDidChangeNotification | |||
object: view]; | |||
} | |||
void detachViewWatcher() | |||
{ | |||
if (callback != nil) | |||
{ | |||
[[NSNotificationCenter defaultCenter] removeObserver: callback]; | |||
[callback release]; | |||
callback = nil; | |||
} | |||
} | |||
virtual void viewResized() = 0; | |||
private: | |||
id callback; | |||
//============================================================================== | |||
struct ViewFrameChangeCallbackClass : public ObjCClass<NSObject> | |||
{ | |||
ViewFrameChangeCallbackClass() : ObjCClass<NSObject> ("JUCE_NSViewCallback_") | |||
{ | |||
addIvar<NSViewResizeWatcher*> ("target"); | |||
addMethod (@selector (frameChanged:), frameChanged, "v@:@"); | |||
registerClass(); | |||
} | |||
static void setTarget (id self, NSViewResizeWatcher* c) | |||
{ | |||
object_setInstanceVariable (self, "target", c); | |||
} | |||
private: | |||
static void frameChanged (id self, SEL, NSNotification*) | |||
{ | |||
if (NSViewResizeWatcher* const target = getIvar<NSViewResizeWatcher*> (self, "target")) | |||
target->viewResized(); | |||
} | |||
JUCE_DECLARE_NON_COPYABLE (ViewFrameChangeCallbackClass) | |||
}; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NSViewResizeWatcher) | |||
}; | |||
//============================================================================== | |||
class NSViewAttachment : public ReferenceCountedObject, | |||
public ComponentMovementWatcher, | |||
private NSViewResizeWatcher | |||
{ | |||
public: | |||
NSViewAttachment (NSView* const v, Component& comp) | |||
: ComponentMovementWatcher (&comp), | |||
view (v), owner (comp), | |||
currentPeer (nullptr) | |||
{ | |||
[view retain]; | |||
[view setPostsFrameChangedNotifications: YES]; | |||
updateAlpha(); | |||
if (owner.isShowing()) | |||
componentPeerChanged(); | |||
attachViewWatcher (view); | |||
} | |||
~NSViewAttachment() | |||
{ | |||
detachViewWatcher(); | |||
removeFromParent(); | |||
[view release]; | |||
} | |||
void componentMovedOrResized (Component& comp, bool wasMoved, bool wasResized) override | |||
{ | |||
ComponentMovementWatcher::componentMovedOrResized (comp, wasMoved, wasResized); | |||
// The ComponentMovementWatcher version of this method avoids calling | |||
// us when the top-level comp is resized, but for an NSView we need to know this | |||
// because with inverted coordinates, we need to update the position even if the | |||
// top-left pos hasn't changed | |||
if (comp.isOnDesktop() && wasResized) | |||
componentMovedOrResized (wasMoved, wasResized); | |||
} | |||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override | |||
{ | |||
if (ComponentPeer* const peer = owner.getTopLevelComponent()->getPeer()) | |||
{ | |||
NSRect r = makeNSRect (peer->getAreaCoveredBy (owner)); | |||
r.origin.y = [[view superview] frame].size.height - (r.origin.y + r.size.height); | |||
[view setFrame: r]; | |||
} | |||
} | |||
void componentPeerChanged() override | |||
{ | |||
ComponentPeer* const peer = owner.getPeer(); | |||
if (currentPeer != peer) | |||
{ | |||
currentPeer = peer; | |||
if (peer != nullptr) | |||
{ | |||
NSView* const peerView = (NSView*) peer->getNativeHandle(); | |||
[peerView addSubview: view]; | |||
componentMovedOrResized (false, false); | |||
} | |||
else | |||
{ | |||
removeFromParent(); | |||
} | |||
} | |||
[view setHidden: ! owner.isShowing()]; | |||
} | |||
void componentVisibilityChanged() override | |||
{ | |||
componentPeerChanged(); | |||
} | |||
void viewResized() override | |||
{ | |||
owner.childBoundsChanged (nullptr); | |||
} | |||
void updateAlpha() | |||
{ | |||
[view setAlphaValue: (CGFloat) owner.getAlpha()]; | |||
} | |||
NSView* const view; | |||
typedef ReferenceCountedObjectPtr<NSViewAttachment> Ptr; | |||
private: | |||
Component& owner; | |||
ComponentPeer* currentPeer; | |||
void removeFromParent() | |||
{ | |||
if ([view superview] != nil) | |||
[view removeFromSuperview]; // Must be careful not to call this unless it's required - e.g. some Apple AU views | |||
// override the call and use it as a sign that they're being deleted, which breaks everything.. | |||
} | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NSViewAttachment) | |||
}; | |||
//============================================================================== | |||
NSViewComponent::NSViewComponent() {} | |||
NSViewComponent::~NSViewComponent() {} | |||
void NSViewComponent::setView (void* const view) | |||
{ | |||
if (view != getView()) | |||
{ | |||
NSViewAttachment::Ptr old = attachment; | |||
attachment = nullptr; | |||
if (view != nullptr) | |||
attachment = attachViewToComponent (*this, view); | |||
old = nullptr; | |||
} | |||
} | |||
void* NSViewComponent::getView() const | |||
{ | |||
return attachment != nullptr ? static_cast<NSViewAttachment*> (attachment.get())->view | |||
: nullptr; | |||
} | |||
void NSViewComponent::resizeToFitView() | |||
{ | |||
if (attachment != nullptr) | |||
{ | |||
NSRect r = [static_cast<NSViewAttachment*> (attachment.get())->view frame]; | |||
setBounds (Rectangle<int> ((int) r.size.width, (int) r.size.height)); | |||
} | |||
} | |||
void NSViewComponent::paint (Graphics&) {} | |||
void NSViewComponent::alphaChanged() | |||
{ | |||
if (attachment != nullptr) | |||
(static_cast<NSViewAttachment*> (attachment.get()))->updateAlpha(); | |||
} | |||
ReferenceCountedObject* NSViewComponent::attachViewToComponent (Component& comp, void* const view) | |||
{ | |||
return new NSViewAttachment ((NSView*) view, comp); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,281 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
namespace MouseCursorHelpers | |||
{ | |||
extern NSImage* createNSImage (const Image&, float scaleFactor = 1.f); | |||
} | |||
extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelMenuId, | |||
int topLevelIndex, bool addDelegate); | |||
class SystemTrayIconComponent::Pimpl : private Timer | |||
{ | |||
public: | |||
Pimpl (SystemTrayIconComponent& iconComp, const Image& im) | |||
: owner (iconComp), statusIcon (MouseCursorHelpers::createNSImage (im)) | |||
{ | |||
static SystemTrayViewClass cls; | |||
view = [cls.createInstance() init]; | |||
SystemTrayViewClass::setOwner (view, this); | |||
SystemTrayViewClass::setImage (view, statusIcon); | |||
setIconSize(); | |||
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]; | |||
[statusItem setView: view]; | |||
SystemTrayViewClass::frameChanged (view, SEL(), nullptr); | |||
[[NSNotificationCenter defaultCenter] addObserver: view | |||
selector: @selector (frameChanged:) | |||
name: NSWindowDidMoveNotification | |||
object: nil]; | |||
} | |||
~Pimpl() | |||
{ | |||
[[NSNotificationCenter defaultCenter] removeObserver: view]; | |||
[[NSStatusBar systemStatusBar] removeStatusItem: statusItem]; | |||
SystemTrayViewClass::setOwner (view, nullptr); | |||
SystemTrayViewClass::setImage (view, nil); | |||
[statusItem release]; | |||
[view release]; | |||
[statusIcon release]; | |||
} | |||
void updateIcon (const Image& newImage) | |||
{ | |||
[statusIcon release]; | |||
statusIcon = MouseCursorHelpers::createNSImage (newImage); | |||
setIconSize(); | |||
SystemTrayViewClass::setImage (view, statusIcon); | |||
[statusItem setView: view]; | |||
} | |||
void setHighlighted (bool shouldHighlight) | |||
{ | |||
isHighlighted = shouldHighlight; | |||
[view setNeedsDisplay: true]; | |||
} | |||
void handleStatusItemAction (NSEvent* e) | |||
{ | |||
NSEventType type = [e type]; | |||
const bool isLeft = (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp); | |||
const bool isRight = (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp); | |||
if (owner.isCurrentlyBlockedByAnotherModalComponent()) | |||
{ | |||
if (isLeft || isRight) | |||
if (auto* current = Component::getCurrentlyModalComponent()) | |||
current->inputAttemptWhenModal(); | |||
} | |||
else | |||
{ | |||
auto eventMods = ModifierKeys::getCurrentModifiersRealtime(); | |||
if (([e modifierFlags] & NSEventModifierFlagCommand) != 0) | |||
eventMods = eventMods.withFlags (ModifierKeys::commandModifier); | |||
auto now = Time::getCurrentTime(); | |||
auto mouseSource = Desktop::getInstance().getMainMouseSource(); | |||
auto pressure = (float) e.pressure; | |||
if (isLeft || isRight) // Only mouse up is sent by the OS, so simulate a down/up | |||
{ | |||
setHighlighted (true); | |||
startTimer (150); | |||
owner.mouseDown (MouseEvent (mouseSource, {}, | |||
eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier | |||
: ModifierKeys::rightButtonModifier), | |||
pressure, MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, | |||
MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, | |||
&owner, &owner, now, {}, now, 1, false)); | |||
owner.mouseUp (MouseEvent (mouseSource, {}, eventMods.withoutMouseButtons(), pressure, | |||
MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, | |||
MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, | |||
&owner, &owner, now, {}, now, 1, false)); | |||
} | |||
else if (type == NSEventTypeMouseMoved) | |||
{ | |||
owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure, | |||
MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, | |||
MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, | |||
&owner, &owner, now, {}, now, 1, false)); | |||
} | |||
} | |||
} | |||
void showMenu (const PopupMenu& menu) | |||
{ | |||
if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true)) | |||
{ | |||
setHighlighted (true); | |||
stopTimer(); | |||
[statusItem popUpStatusItemMenu: m]; | |||
startTimer (1); | |||
} | |||
} | |||
SystemTrayIconComponent& owner; | |||
NSStatusItem* statusItem = nil; | |||
private: | |||
NSImage* statusIcon = nil; | |||
NSControl* view = nil; | |||
bool isHighlighted = false; | |||
void setIconSize() | |||
{ | |||
[statusIcon setSize: NSMakeSize (20.0f, 20.0f)]; | |||
} | |||
void timerCallback() override | |||
{ | |||
stopTimer(); | |||
setHighlighted (false); | |||
} | |||
struct SystemTrayViewClass : public ObjCClass<NSControl> | |||
{ | |||
SystemTrayViewClass() : ObjCClass<NSControl> ("JUCESystemTrayView_") | |||
{ | |||
addIvar<Pimpl*> ("owner"); | |||
addIvar<NSImage*> ("image"); | |||
addMethod (@selector (mouseDown:), handleEventDown, "v@:@"); | |||
addMethod (@selector (rightMouseDown:), handleEventDown, "v@:@"); | |||
addMethod (@selector (drawRect:), drawRect, "v@:@"); | |||
addMethod (@selector (frameChanged:), frameChanged, "v@:@"); | |||
registerClass(); | |||
} | |||
static Pimpl* getOwner (id self) { return getIvar<Pimpl*> (self, "owner"); } | |||
static NSImage* getImage (id self) { return getIvar<NSImage*> (self, "image"); } | |||
static void setOwner (id self, Pimpl* owner) { object_setInstanceVariable (self, "owner", owner); } | |||
static void setImage (id self, NSImage* image) { object_setInstanceVariable (self, "image", image); } | |||
static void frameChanged (id self, SEL, NSNotification*) | |||
{ | |||
if (auto* owner = getOwner (self)) | |||
{ | |||
NSRect r = [[[owner->statusItem view] window] frame]; | |||
NSRect sr = [[[NSScreen screens] objectAtIndex: 0] frame]; | |||
r.origin.y = sr.size.height - r.origin.y - r.size.height; | |||
owner->owner.setBounds (convertToRectInt (r)); | |||
} | |||
} | |||
private: | |||
static void handleEventDown (id self, SEL, NSEvent* e) | |||
{ | |||
if (auto* owner = getOwner (self)) | |||
owner->handleStatusItemAction (e); | |||
} | |||
static void drawRect (id self, SEL, NSRect) | |||
{ | |||
NSRect bounds = [self bounds]; | |||
if (auto* owner = getOwner (self)) | |||
[owner->statusItem drawStatusBarBackgroundInRect: bounds | |||
withHighlight: owner->isHighlighted]; | |||
if (NSImage* const im = getImage (self)) | |||
{ | |||
NSSize imageSize = [im size]; | |||
[im drawInRect: NSMakeRect (bounds.origin.x + ((bounds.size.width - imageSize.width) / 2.0f), | |||
bounds.origin.y + ((bounds.size.height - imageSize.height) / 2.0f), | |||
imageSize.width, imageSize.height) | |||
fromRect: NSZeroRect | |||
operation: NSCompositingOperationSourceOver | |||
fraction: 1.0f]; | |||
} | |||
} | |||
}; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | |||
}; | |||
//============================================================================== | |||
void SystemTrayIconComponent::setIconImage (const Image& newImage) | |||
{ | |||
if (newImage.isValid()) | |||
{ | |||
if (pimpl == nullptr) | |||
pimpl = new Pimpl (*this, newImage); | |||
else | |||
pimpl->updateIcon (newImage); | |||
} | |||
else | |||
{ | |||
pimpl = nullptr; | |||
} | |||
} | |||
void SystemTrayIconComponent::setIconTooltip (const String&) | |||
{ | |||
// xxx not yet implemented! | |||
} | |||
void SystemTrayIconComponent::setHighlighted (bool highlight) | |||
{ | |||
if (pimpl != nullptr) | |||
pimpl->setHighlighted (highlight); | |||
} | |||
void SystemTrayIconComponent::showInfoBubble (const String& /*title*/, const String& /*content*/) | |||
{ | |||
// xxx Not implemented! | |||
} | |||
void SystemTrayIconComponent::hideInfoBubble() | |||
{ | |||
// xxx Not implemented! | |||
} | |||
void* SystemTrayIconComponent::getNativeHandle() const | |||
{ | |||
return pimpl != nullptr ? pimpl->statusItem : nullptr; | |||
} | |||
void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu) | |||
{ | |||
if (pimpl != nullptr) | |||
pimpl->showMenu (menu); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,491 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
#if JUCE_MAC | |||
namespace juce | |||
{ | |||
struct WebViewKeyEquivalentResponder : public ObjCClass<WebView> | |||
{ | |||
WebViewKeyEquivalentResponder() : ObjCClass<WebView> ("WebViewKeyEquivalentResponder_") | |||
{ | |||
addMethod (@selector (performKeyEquivalent:), performKeyEquivalent, @encode (BOOL), "@:@"); | |||
registerClass(); | |||
} | |||
private: | |||
static BOOL performKeyEquivalent (id self, SEL selector, NSEvent* event) | |||
{ | |||
NSResponder* first = [[self window] firstResponder]; | |||
#if (defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12) | |||
if (([event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagCommand) | |||
#else | |||
if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask) | |||
#endif | |||
{ | |||
if ([[event charactersIgnoringModifiers] isEqualToString:@"x"]) return [NSApp sendAction:@selector(cut:) to:first from:self]; | |||
if ([[event charactersIgnoringModifiers] isEqualToString:@"c"]) return [NSApp sendAction:@selector(copy:) to:first from:self]; | |||
if ([[event charactersIgnoringModifiers] isEqualToString:@"v"]) return [NSApp sendAction:@selector(paste:) to:first from:self]; | |||
if ([[event charactersIgnoringModifiers] isEqualToString:@"a"]) return [NSApp sendAction:@selector(selectAll:) to:first from:self]; | |||
} | |||
objc_super s = { self, [WebView class] }; | |||
return ObjCMsgSendSuper<BOOL, NSEvent*> (&s, selector, event); | |||
} | |||
}; | |||
struct DownloadClickDetectorClass : public ObjCClass<NSObject> | |||
{ | |||
DownloadClickDetectorClass() : ObjCClass<NSObject> ("JUCEWebClickDetector_") | |||
{ | |||
addIvar<WebBrowserComponent*> ("owner"); | |||
addMethod (@selector (webView:decidePolicyForNavigationAction:request:frame:decisionListener:), | |||
decidePolicyForNavigationAction, "v@:@@@@@"); | |||
addMethod (@selector (webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener:), | |||
decidePolicyForNewWindowAction, "v@:@@@@@"); | |||
addMethod (@selector (webView:didFinishLoadForFrame:), didFinishLoadForFrame, "v@:@@"); | |||
addMethod (@selector (webView:didFailLoadWithError:forFrame:), didFailLoadWithError, "v@:@@@"); | |||
addMethod (@selector (webView:didFailProvisionalLoadWithError:forFrame:), didFailLoadWithError, "v@:@@@"); | |||
addMethod (@selector (webView:willCloseFrame:), willCloseFrame, "v@:@@"); | |||
addMethod (@selector (webView:runOpenPanelForFileButtonWithResultListener:allowMultipleFiles:), runOpenPanel, "v@:@@", @encode (BOOL)); | |||
registerClass(); | |||
} | |||
static void setOwner (id self, WebBrowserComponent* owner) { object_setInstanceVariable (self, "owner", owner); } | |||
static WebBrowserComponent* getOwner (id self) { return getIvar<WebBrowserComponent*> (self, "owner"); } | |||
private: | |||
static String getOriginalURL (NSDictionary* actionInformation) | |||
{ | |||
if (NSURL* url = [actionInformation valueForKey: nsStringLiteral ("WebActionOriginalURLKey")]) | |||
return nsStringToJuce ([url absoluteString]); | |||
return {}; | |||
} | |||
static void decidePolicyForNavigationAction (id self, SEL, WebView*, NSDictionary* actionInformation, | |||
NSURLRequest*, WebFrame*, id<WebPolicyDecisionListener> listener) | |||
{ | |||
if (getOwner (self)->pageAboutToLoad (getOriginalURL (actionInformation))) | |||
[listener use]; | |||
else | |||
[listener ignore]; | |||
} | |||
static void decidePolicyForNewWindowAction (id self, SEL, WebView*, NSDictionary* actionInformation, | |||
NSURLRequest*, NSString*, id<WebPolicyDecisionListener> listener) | |||
{ | |||
getOwner (self)->newWindowAttemptingToLoad (getOriginalURL (actionInformation)); | |||
[listener ignore]; | |||
} | |||
static void didFinishLoadForFrame (id self, SEL, WebView* sender, WebFrame* frame) | |||
{ | |||
if ([frame isEqual: [sender mainFrame]]) | |||
{ | |||
NSURL* url = [[[frame dataSource] request] URL]; | |||
getOwner (self)->pageFinishedLoading (nsStringToJuce ([url absoluteString])); | |||
} | |||
} | |||
static void didFailLoadWithError (id self, SEL, WebView* sender, NSError* error, WebFrame* frame) | |||
{ | |||
if ([frame isEqual: [sender mainFrame]] && error != nullptr && [error code] != NSURLErrorCancelled) | |||
{ | |||
String errorString (nsStringToJuce ([error localizedDescription])); | |||
bool proceedToErrorPage = getOwner (self)->pageLoadHadNetworkError (errorString); | |||
// WebKit doesn't have an internal error page, so make a really simple one ourselves | |||
if (proceedToErrorPage) | |||
getOwner(self)->goToURL (String ("data:text/plain;charset=UTF-8,") + errorString); | |||
} | |||
} | |||
static void willCloseFrame (id self, SEL, WebView*, WebFrame*) | |||
{ | |||
getOwner (self)->windowCloseRequest(); | |||
} | |||
static void runOpenPanel (id, SEL, WebView*, id<WebOpenPanelResultListener> resultListener, BOOL allowMultipleFiles) | |||
{ | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
FileChooser chooser (TRANS("Select the file you want to upload..."), | |||
File::getSpecialLocation (File::userHomeDirectory), "*"); | |||
if (allowMultipleFiles ? chooser.browseForMultipleFilesToOpen() | |||
: chooser.browseForFileToOpen()) | |||
{ | |||
for (auto& f : chooser.getResults()) | |||
[resultListener chooseFilename: juceStringToNS (f.getFullPathName())]; | |||
} | |||
#else | |||
ignoreUnused (resultListener, allowMultipleFiles); | |||
jassertfalse; // Can't use this without modal loops being enabled! | |||
#endif | |||
} | |||
}; | |||
#else | |||
//============================================================================== | |||
@interface WebViewTapDetector : NSObject<UIGestureRecognizerDelegate> | |||
{ | |||
} | |||
- (BOOL) gestureRecognizer: (UIGestureRecognizer*) gestureRecognizer | |||
shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer*) otherGestureRecognizer; | |||
@end | |||
@implementation WebViewTapDetector | |||
- (BOOL) gestureRecognizer: (UIGestureRecognizer*) gestureRecognizer | |||
shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer*) otherGestureRecognizer | |||
{ | |||
juce::ignoreUnused (gestureRecognizer, otherGestureRecognizer); | |||
return YES; | |||
} | |||
@end | |||
//============================================================================== | |||
@interface WebViewURLChangeDetector : NSObject<UIWebViewDelegate> | |||
{ | |||
juce::WebBrowserComponent* ownerComponent; | |||
} | |||
- (WebViewURLChangeDetector*) initWithWebBrowserOwner: (juce::WebBrowserComponent*) ownerComponent; | |||
- (BOOL) webView: (UIWebView*) webView shouldStartLoadWithRequest: (NSURLRequest*) request | |||
navigationType: (UIWebViewNavigationType) navigationType; | |||
- (void) webViewDidFinishLoad: (UIWebView*) webView; | |||
@end | |||
@implementation WebViewURLChangeDetector | |||
- (WebViewURLChangeDetector*) initWithWebBrowserOwner: (juce::WebBrowserComponent*) ownerComp | |||
{ | |||
[super init]; | |||
ownerComponent = ownerComp; | |||
return self; | |||
} | |||
- (BOOL) webView: (UIWebView*) webView shouldStartLoadWithRequest: (NSURLRequest*) request | |||
navigationType: (UIWebViewNavigationType) navigationType | |||
{ | |||
juce::ignoreUnused (webView, navigationType); | |||
return ownerComponent->pageAboutToLoad (juce::nsStringToJuce (request.URL.absoluteString)); | |||
} | |||
- (void) webViewDidFinishLoad: (UIWebView*) webView | |||
{ | |||
ownerComponent->pageFinishedLoading (juce::nsStringToJuce (webView.request.URL.absoluteString)); | |||
} | |||
@end | |||
namespace juce | |||
{ | |||
#endif | |||
//============================================================================== | |||
class WebBrowserComponent::Pimpl | |||
#if JUCE_MAC | |||
: public NSViewComponent | |||
#else | |||
: public UIViewComponent | |||
#endif | |||
{ | |||
public: | |||
Pimpl (WebBrowserComponent* owner) | |||
{ | |||
#if JUCE_MAC | |||
static WebViewKeyEquivalentResponder webviewClass; | |||
webView = (WebView*) webviewClass.createInstance(); | |||
webView = [webView initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f) | |||
frameName: nsEmptyString() | |||
groupName: nsEmptyString()]; | |||
setView (webView); | |||
static DownloadClickDetectorClass cls; | |||
clickListener = [cls.createInstance() init]; | |||
DownloadClickDetectorClass::setOwner (clickListener, owner); | |||
[webView setPolicyDelegate: clickListener]; | |||
[webView setFrameLoadDelegate: clickListener]; | |||
[webView setUIDelegate: clickListener]; | |||
#else | |||
webView = [[UIWebView alloc] initWithFrame: CGRectMake (0, 0, 1.0f, 1.0f)]; | |||
setView (webView); | |||
tapDetector = [[WebViewTapDetector alloc] init]; | |||
urlDetector = [[WebViewURLChangeDetector alloc] initWithWebBrowserOwner: owner]; | |||
gestureRecogniser = nil; | |||
webView.delegate = urlDetector; | |||
#endif | |||
} | |||
~Pimpl() | |||
{ | |||
#if JUCE_MAC | |||
[webView setPolicyDelegate: nil]; | |||
[webView setFrameLoadDelegate: nil]; | |||
[webView setUIDelegate: nil]; | |||
[clickListener release]; | |||
#else | |||
webView.delegate = nil; | |||
[webView removeGestureRecognizer: gestureRecogniser]; | |||
[gestureRecogniser release]; | |||
[tapDetector release]; | |||
[urlDetector release]; | |||
#endif | |||
setView (nil); | |||
} | |||
void goToURL (const String& url, | |||
const StringArray* headers, | |||
const MemoryBlock* postData) | |||
{ | |||
stop(); | |||
if (url.trimStart().startsWithIgnoreCase ("javascript:")) | |||
{ | |||
[webView stringByEvaluatingJavaScriptFromString: | |||
juceStringToNS (url.fromFirstOccurrenceOf (":", false, false))]; | |||
} | |||
else | |||
{ | |||
NSString* urlString = juceStringToNS (url); | |||
#if (JUCE_MAC && (defined (__MAC_OS_X_VERSION_MIN_REQUIRED) && defined (__MAC_10_9) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9)) || (JUCE_IOS && (defined (__IPHONE_OS_VERSION_MIN_REQUIRED) && defined (__IPHONE_7_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0)) | |||
urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; | |||
#else | |||
urlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; | |||
#endif | |||
NSMutableURLRequest* r | |||
= [NSMutableURLRequest requestWithURL: [NSURL URLWithString: urlString] | |||
cachePolicy: NSURLRequestUseProtocolCachePolicy | |||
timeoutInterval: 30.0]; | |||
if (postData != nullptr && postData->getSize() > 0) | |||
{ | |||
[r setHTTPMethod: nsStringLiteral ("POST")]; | |||
[r setHTTPBody: [NSData dataWithBytes: postData->getData() | |||
length: postData->getSize()]]; | |||
} | |||
if (headers != nullptr) | |||
{ | |||
for (int i = 0; i < headers->size(); ++i) | |||
{ | |||
const String headerName ((*headers)[i].upToFirstOccurrenceOf (":", false, false).trim()); | |||
const String headerValue ((*headers)[i].fromFirstOccurrenceOf (":", false, false).trim()); | |||
[r setValue: juceStringToNS (headerValue) | |||
forHTTPHeaderField: juceStringToNS (headerName)]; | |||
} | |||
} | |||
#if JUCE_MAC | |||
[[webView mainFrame] loadRequest: r]; | |||
#else | |||
[webView loadRequest: r]; | |||
#endif | |||
#if JUCE_IOS | |||
[webView setScalesPageToFit:YES]; | |||
#endif | |||
} | |||
} | |||
void goBack() { [webView goBack]; } | |||
void goForward() { [webView goForward]; } | |||
#if JUCE_MAC | |||
void stop() { [webView stopLoading: nil]; } | |||
void refresh() { [webView reload: nil]; } | |||
#else | |||
void stop() { [webView stopLoading]; } | |||
void refresh() { [webView reload]; } | |||
#endif | |||
void mouseMove (const MouseEvent&) | |||
{ | |||
// WebKit doesn't capture mouse-moves itself, so it seems the only way to make | |||
// them work is to push them via this non-public method.. | |||
if ([webView respondsToSelector: @selector (_updateMouseoverWithFakeEvent)]) | |||
[webView performSelector: @selector (_updateMouseoverWithFakeEvent)]; | |||
} | |||
private: | |||
#if JUCE_MAC | |||
WebView* webView; | |||
id clickListener; | |||
#else | |||
UIWebView* webView; | |||
WebViewTapDetector* tapDetector; | |||
WebViewURLChangeDetector* urlDetector; | |||
UITapGestureRecognizer* gestureRecogniser; | |||
#endif | |||
}; | |||
//============================================================================== | |||
WebBrowserComponent::WebBrowserComponent (const bool unloadWhenHidden) | |||
: browser (nullptr), | |||
blankPageShown (false), | |||
unloadPageWhenBrowserIsHidden (unloadWhenHidden) | |||
{ | |||
setOpaque (true); | |||
addAndMakeVisible (browser = new Pimpl (this)); | |||
} | |||
WebBrowserComponent::~WebBrowserComponent() | |||
{ | |||
deleteAndZero (browser); | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::goToURL (const String& url, | |||
const StringArray* headers, | |||
const MemoryBlock* postData) | |||
{ | |||
lastURL = url; | |||
if (headers != nullptr) | |||
lastHeaders = *headers; | |||
else | |||
lastHeaders.clear(); | |||
if (postData != nullptr) | |||
lastPostData = *postData; | |||
else | |||
lastPostData.reset(); | |||
blankPageShown = false; | |||
browser->goToURL (url, headers, postData); | |||
} | |||
void WebBrowserComponent::stop() | |||
{ | |||
browser->stop(); | |||
} | |||
void WebBrowserComponent::goBack() | |||
{ | |||
lastURL.clear(); | |||
blankPageShown = false; | |||
browser->goBack(); | |||
} | |||
void WebBrowserComponent::goForward() | |||
{ | |||
lastURL.clear(); | |||
browser->goForward(); | |||
} | |||
void WebBrowserComponent::refresh() | |||
{ | |||
browser->refresh(); | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::paint (Graphics&) | |||
{ | |||
} | |||
void WebBrowserComponent::checkWindowAssociation() | |||
{ | |||
if (isShowing()) | |||
{ | |||
reloadLastURL(); | |||
if (blankPageShown) | |||
goBack(); | |||
} | |||
else | |||
{ | |||
if (unloadPageWhenBrowserIsHidden && ! blankPageShown) | |||
{ | |||
// when the component becomes invisible, some stuff like flash | |||
// carries on playing audio, so we need to force it onto a blank | |||
// page to avoid this, (and send it back when it's made visible again). | |||
blankPageShown = true; | |||
browser->goToURL ("about:blank", 0, 0); | |||
} | |||
} | |||
} | |||
void WebBrowserComponent::reloadLastURL() | |||
{ | |||
if (lastURL.isNotEmpty()) | |||
{ | |||
goToURL (lastURL, &lastHeaders, &lastPostData); | |||
lastURL.clear(); | |||
} | |||
} | |||
void WebBrowserComponent::parentHierarchyChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::resized() | |||
{ | |||
browser->setSize (getWidth(), getHeight()); | |||
} | |||
void WebBrowserComponent::visibilityChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::focusGained (FocusChangeType) | |||
{ | |||
} | |||
void WebBrowserComponent::clearCookies() | |||
{ | |||
NSHTTPCookieStorage* storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; | |||
if (NSArray* cookies = [storage cookies]) | |||
{ | |||
const NSUInteger n = [cookies count]; | |||
for (NSUInteger i = 0; i < n; ++i) | |||
[storage deleteCookie: [cookies objectAtIndex: i]]; | |||
} | |||
[[NSUserDefaults standardUserDefaults] synchronize]; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,458 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
extern int64 getMouseEventTime(); | |||
JUCE_DECLARE_UUID_GETTER (IOleObject, "00000112-0000-0000-C000-000000000046") | |||
JUCE_DECLARE_UUID_GETTER (IOleWindow, "00000114-0000-0000-C000-000000000046") | |||
JUCE_DECLARE_UUID_GETTER (IOleInPlaceSite, "00000119-0000-0000-C000-000000000046") | |||
namespace ActiveXHelpers | |||
{ | |||
//============================================================================== | |||
struct JuceIStorage : public ComBaseClassHelper<IStorage> | |||
{ | |||
JuceIStorage() {} | |||
JUCE_COMRESULT CreateStream (const WCHAR*, DWORD, DWORD, DWORD, IStream**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT OpenStream (const WCHAR*, void*, DWORD, DWORD, IStream**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT CreateStorage (const WCHAR*, DWORD, DWORD, DWORD, IStorage**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT OpenStorage (const WCHAR*, IStorage*, DWORD, SNB, DWORD, IStorage**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT CopyTo (DWORD, IID const*, SNB, IStorage*) { return E_NOTIMPL; } | |||
JUCE_COMRESULT MoveElementTo (const OLECHAR*,IStorage*, const OLECHAR*, DWORD) { return E_NOTIMPL; } | |||
JUCE_COMRESULT Commit (DWORD) { return E_NOTIMPL; } | |||
JUCE_COMRESULT Revert() { return E_NOTIMPL; } | |||
JUCE_COMRESULT EnumElements (DWORD, void*, DWORD, IEnumSTATSTG**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT DestroyElement (const OLECHAR*) { return E_NOTIMPL; } | |||
JUCE_COMRESULT RenameElement (const WCHAR*, const WCHAR*) { return E_NOTIMPL; } | |||
JUCE_COMRESULT SetElementTimes (const WCHAR*, FILETIME const*, FILETIME const*, FILETIME const*) { return E_NOTIMPL; } | |||
JUCE_COMRESULT SetClass (REFCLSID) { return S_OK; } | |||
JUCE_COMRESULT SetStateBits (DWORD, DWORD) { return E_NOTIMPL; } | |||
JUCE_COMRESULT Stat (STATSTG*, DWORD) { return E_NOTIMPL; } | |||
}; | |||
//============================================================================== | |||
struct JuceOleInPlaceFrame : public ComBaseClassHelper<IOleInPlaceFrame> | |||
{ | |||
JuceOleInPlaceFrame (HWND hwnd) : window (hwnd) {} | |||
JUCE_COMRESULT GetWindow (HWND* lphwnd) { *lphwnd = window; return S_OK; } | |||
JUCE_COMRESULT ContextSensitiveHelp (BOOL) { return E_NOTIMPL; } | |||
JUCE_COMRESULT GetBorder (LPRECT) { return E_NOTIMPL; } | |||
JUCE_COMRESULT RequestBorderSpace (LPCBORDERWIDTHS) { return E_NOTIMPL; } | |||
JUCE_COMRESULT SetBorderSpace (LPCBORDERWIDTHS) { return E_NOTIMPL; } | |||
JUCE_COMRESULT SetActiveObject (IOleInPlaceActiveObject* a, LPCOLESTR) { activeObject = a; return S_OK; } | |||
JUCE_COMRESULT InsertMenus (HMENU, LPOLEMENUGROUPWIDTHS) { return E_NOTIMPL; } | |||
JUCE_COMRESULT SetMenu (HMENU, HOLEMENU, HWND) { return S_OK; } | |||
JUCE_COMRESULT RemoveMenus (HMENU) { return E_NOTIMPL; } | |||
JUCE_COMRESULT SetStatusText (LPCOLESTR) { return S_OK; } | |||
JUCE_COMRESULT EnableModeless (BOOL) { return S_OK; } | |||
JUCE_COMRESULT TranslateAccelerator (LPMSG, WORD) { return E_NOTIMPL; } | |||
HRESULT OfferKeyTranslation (LPMSG lpmsg) | |||
{ | |||
if (activeObject != nullptr) | |||
return activeObject->TranslateAcceleratorW (lpmsg); | |||
return S_FALSE; | |||
} | |||
HWND window; | |||
ComSmartPtr<IOleInPlaceActiveObject> activeObject; | |||
}; | |||
//============================================================================== | |||
struct JuceIOleInPlaceSite : public ComBaseClassHelper<IOleInPlaceSite> | |||
{ | |||
JuceIOleInPlaceSite (HWND hwnd) | |||
: window (hwnd), | |||
frame (new JuceOleInPlaceFrame (window)) | |||
{} | |||
~JuceIOleInPlaceSite() | |||
{ | |||
frame->Release(); | |||
} | |||
JUCE_COMRESULT GetWindow (HWND* lphwnd) { *lphwnd = window; return S_OK; } | |||
JUCE_COMRESULT ContextSensitiveHelp (BOOL) { return E_NOTIMPL; } | |||
JUCE_COMRESULT CanInPlaceActivate() { return S_OK; } | |||
JUCE_COMRESULT OnInPlaceActivate() { return S_OK; } | |||
JUCE_COMRESULT OnUIActivate() { return S_OK; } | |||
JUCE_COMRESULT GetWindowContext (LPOLEINPLACEFRAME* lplpFrame, LPOLEINPLACEUIWINDOW* lplpDoc, LPRECT, LPRECT, LPOLEINPLACEFRAMEINFO lpFrameInfo) | |||
{ | |||
/* Note: if you call AddRef on the frame here, then some types of object (e.g. web browser control) cause leaks.. | |||
If you don't call AddRef then others crash (e.g. QuickTime).. Bit of a catch-22, so letting it leak is probably preferable. | |||
*/ | |||
if (lplpFrame != nullptr) { frame->AddRef(); *lplpFrame = frame; } | |||
if (lplpDoc != nullptr) *lplpDoc = nullptr; | |||
lpFrameInfo->fMDIApp = FALSE; | |||
lpFrameInfo->hwndFrame = window; | |||
lpFrameInfo->haccel = 0; | |||
lpFrameInfo->cAccelEntries = 0; | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT Scroll (SIZE) { return E_NOTIMPL; } | |||
JUCE_COMRESULT OnUIDeactivate (BOOL) { return S_OK; } | |||
JUCE_COMRESULT OnInPlaceDeactivate() { return S_OK; } | |||
JUCE_COMRESULT DiscardUndoState() { return E_NOTIMPL; } | |||
JUCE_COMRESULT DeactivateAndUndo() { return E_NOTIMPL; } | |||
JUCE_COMRESULT OnPosRectChange (LPCRECT) { return S_OK; } | |||
LRESULT offerEventToActiveXControl (::MSG& msg) | |||
{ | |||
if (frame != nullptr) | |||
return frame->OfferKeyTranslation (&msg); | |||
return S_FALSE; | |||
} | |||
HWND window; | |||
JuceOleInPlaceFrame* frame; | |||
}; | |||
//============================================================================== | |||
struct JuceIOleClientSite : public ComBaseClassHelper<IOleClientSite> | |||
{ | |||
JuceIOleClientSite (HWND window) : inplaceSite (new JuceIOleInPlaceSite (window)) | |||
{} | |||
~JuceIOleClientSite() | |||
{ | |||
inplaceSite->Release(); | |||
} | |||
JUCE_COMRESULT QueryInterface (REFIID type, void** result) | |||
{ | |||
if (type == __uuidof (IOleInPlaceSite)) | |||
{ | |||
inplaceSite->AddRef(); | |||
*result = static_cast<IOleInPlaceSite*> (inplaceSite); | |||
return S_OK; | |||
} | |||
return ComBaseClassHelper <IOleClientSite>::QueryInterface (type, result); | |||
} | |||
JUCE_COMRESULT SaveObject() { return E_NOTIMPL; } | |||
JUCE_COMRESULT GetMoniker (DWORD, DWORD, IMoniker**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT GetContainer (LPOLECONTAINER* ppContainer) { *ppContainer = nullptr; return E_NOINTERFACE; } | |||
JUCE_COMRESULT ShowObject() { return S_OK; } | |||
JUCE_COMRESULT OnShowWindow (BOOL) { return E_NOTIMPL; } | |||
JUCE_COMRESULT RequestNewObjectLayout() { return E_NOTIMPL; } | |||
LRESULT offerEventToActiveXControl (::MSG& msg) | |||
{ | |||
if (inplaceSite != nullptr) | |||
return inplaceSite->offerEventToActiveXControl (msg); | |||
return S_FALSE; | |||
} | |||
JuceIOleInPlaceSite* inplaceSite; | |||
}; | |||
//============================================================================== | |||
static Array<ActiveXControlComponent*> activeXComps; | |||
static inline HWND getHWND (const ActiveXControlComponent* const component) | |||
{ | |||
HWND hwnd = {}; | |||
const IID iid = __uuidof (IOleWindow); | |||
if (auto* window = (IOleWindow*) component->queryInterface (&iid)) | |||
{ | |||
window->GetWindow (&hwnd); | |||
window->Release(); | |||
} | |||
return hwnd; | |||
} | |||
static inline void offerActiveXMouseEventToPeer (ComponentPeer* peer, HWND hwnd, UINT message, LPARAM lParam) | |||
{ | |||
switch (message) | |||
{ | |||
case WM_MOUSEMOVE: | |||
case WM_LBUTTONDOWN: | |||
case WM_MBUTTONDOWN: | |||
case WM_RBUTTONDOWN: | |||
case WM_LBUTTONUP: | |||
case WM_MBUTTONUP: | |||
case WM_RBUTTONUP: | |||
{ | |||
RECT activeXRect, peerRect; | |||
GetWindowRect (hwnd, &activeXRect); | |||
GetWindowRect ((HWND) peer->getNativeHandle(), &peerRect); | |||
peer->handleMouseEvent (MouseInputSource::InputSourceType::mouse, | |||
{ (float) (GET_X_LPARAM (lParam) + activeXRect.left - peerRect.left), | |||
(float) (GET_Y_LPARAM (lParam) + activeXRect.top - peerRect.top) }, | |||
ModifierKeys::getCurrentModifiersRealtime(), | |||
MouseInputSource::invalidPressure, | |||
MouseInputSource::invalidOrientation, | |||
getMouseEventTime()); | |||
break; | |||
} | |||
default: | |||
break; | |||
} | |||
} | |||
} | |||
//============================================================================== | |||
class ActiveXControlComponent::Pimpl : public ComponentMovementWatcher | |||
{ | |||
public: | |||
Pimpl (HWND hwnd, ActiveXControlComponent& activeXComp) | |||
: ComponentMovementWatcher (&activeXComp), | |||
owner (activeXComp), | |||
storage (new ActiveXHelpers::JuceIStorage()), | |||
clientSite (new ActiveXHelpers::JuceIOleClientSite (hwnd)) | |||
{ | |||
} | |||
~Pimpl() | |||
{ | |||
if (control != nullptr) | |||
{ | |||
control->Close (OLECLOSE_NOSAVE); | |||
control->Release(); | |||
} | |||
clientSite->Release(); | |||
storage->Release(); | |||
} | |||
void setControlBounds (Rectangle<int> newBounds) const | |||
{ | |||
if (controlHWND != 0) | |||
MoveWindow (controlHWND, newBounds.getX(), newBounds.getY(), newBounds.getWidth(), newBounds.getHeight(), TRUE); | |||
} | |||
void setControlVisible (bool shouldBeVisible) const | |||
{ | |||
if (controlHWND != 0) | |||
ShowWindow (controlHWND, shouldBeVisible ? SW_SHOWNA : SW_HIDE); | |||
} | |||
//============================================================================== | |||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override | |||
{ | |||
if (auto* peer = owner.getTopLevelComponent()->getPeer()) | |||
setControlBounds (peer->getAreaCoveredBy(owner)); | |||
} | |||
void componentPeerChanged() override | |||
{ | |||
componentMovedOrResized (true, true); | |||
} | |||
void componentVisibilityChanged() override | |||
{ | |||
setControlVisible (owner.isShowing()); | |||
componentPeerChanged(); | |||
} | |||
// intercepts events going to an activeX control, so we can sneakily use the mouse events | |||
static LRESULT CALLBACK activeXHookWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) | |||
{ | |||
for (auto* ax : ActiveXHelpers::activeXComps) | |||
{ | |||
if (ax->control != nullptr && ax->control->controlHWND == hwnd) | |||
{ | |||
switch (message) | |||
{ | |||
case WM_MOUSEMOVE: | |||
case WM_LBUTTONDOWN: | |||
case WM_MBUTTONDOWN: | |||
case WM_RBUTTONDOWN: | |||
case WM_LBUTTONUP: | |||
case WM_MBUTTONUP: | |||
case WM_RBUTTONUP: | |||
case WM_LBUTTONDBLCLK: | |||
case WM_MBUTTONDBLCLK: | |||
case WM_RBUTTONDBLCLK: | |||
if (ax->isShowing()) | |||
{ | |||
if (auto* peer = ax->getPeer()) | |||
{ | |||
ActiveXHelpers::offerActiveXMouseEventToPeer (peer, hwnd, message, lParam); | |||
if (! ax->areMouseEventsAllowed()) | |||
return 0; | |||
} | |||
} | |||
break; | |||
default: | |||
break; | |||
} | |||
return CallWindowProc (ax->control->originalWndProc, hwnd, message, wParam, lParam); | |||
} | |||
} | |||
return DefWindowProc (hwnd, message, wParam, lParam); | |||
} | |||
ActiveXControlComponent& owner; | |||
HWND controlHWND = {}; | |||
IStorage* storage = nullptr; | |||
ActiveXHelpers::JuceIOleClientSite* clientSite = nullptr; | |||
IOleObject* control = nullptr; | |||
WNDPROC originalWndProc = 0; | |||
}; | |||
//============================================================================== | |||
ActiveXControlComponent::ActiveXControlComponent() | |||
{ | |||
ActiveXHelpers::activeXComps.add (this); | |||
} | |||
ActiveXControlComponent::~ActiveXControlComponent() | |||
{ | |||
deleteControl(); | |||
ActiveXHelpers::activeXComps.removeFirstMatchingValue (this); | |||
} | |||
void ActiveXControlComponent::paint (Graphics& g) | |||
{ | |||
if (control == nullptr) | |||
g.fillAll (Colours::lightgrey); | |||
} | |||
bool ActiveXControlComponent::createControl (const void* controlIID) | |||
{ | |||
deleteControl(); | |||
if (auto* peer = getPeer()) | |||
{ | |||
auto controlBounds = peer->getAreaCoveredBy (*this); | |||
auto hwnd = (HWND) peer->getNativeHandle(); | |||
ScopedPointer<Pimpl> newControl (new Pimpl (hwnd, *this)); | |||
HRESULT hr = OleCreate (*(const IID*) controlIID, __uuidof (IOleObject), 1 /*OLERENDER_DRAW*/, 0, | |||
newControl->clientSite, newControl->storage, | |||
(void**) &(newControl->control)); | |||
if (hr == S_OK) | |||
{ | |||
newControl->control->SetHostNames (L"JUCE", 0); | |||
if (OleSetContainedObject (newControl->control, TRUE) == S_OK) | |||
{ | |||
RECT rect; | |||
rect.left = controlBounds.getX(); | |||
rect.top = controlBounds.getY(); | |||
rect.right = controlBounds.getRight(); | |||
rect.bottom = controlBounds.getBottom(); | |||
if (newControl->control->DoVerb (OLEIVERB_SHOW, 0, newControl->clientSite, 0, hwnd, &rect) == S_OK) | |||
{ | |||
control = newControl; | |||
control->controlHWND = ActiveXHelpers::getHWND (this); | |||
if (control->controlHWND != 0) | |||
{ | |||
control->setControlBounds (controlBounds); | |||
control->originalWndProc = (WNDPROC) GetWindowLongPtr ((HWND) control->controlHWND, GWLP_WNDPROC); | |||
SetWindowLongPtr ((HWND) control->controlHWND, GWLP_WNDPROC, (LONG_PTR) Pimpl::activeXHookWndProc); | |||
} | |||
return true; | |||
} | |||
} | |||
} | |||
} | |||
else | |||
{ | |||
// the component must have already been added to a real window when you call this! | |||
jassertfalse; | |||
} | |||
return false; | |||
} | |||
void ActiveXControlComponent::deleteControl() | |||
{ | |||
control = nullptr; | |||
} | |||
void* ActiveXControlComponent::queryInterface (const void* iid) const | |||
{ | |||
void* result = nullptr; | |||
if (control != nullptr && control->control != nullptr | |||
&& SUCCEEDED (control->control->QueryInterface (*(const IID*) iid, &result))) | |||
return result; | |||
return nullptr; | |||
} | |||
void ActiveXControlComponent::setMouseEventsAllowed (const bool eventsCanReachControl) | |||
{ | |||
mouseEventsAllowed = eventsCanReachControl; | |||
} | |||
intptr_t ActiveXControlComponent::offerEventToActiveXControl (void* ptr) | |||
{ | |||
if (control != nullptr && control->clientSite != nullptr) | |||
return (intptr_t) control->clientSite->offerEventToActiveXControl (*reinterpret_cast<::MSG*> (ptr)); | |||
return S_FALSE; | |||
} | |||
intptr_t ActiveXControlComponent::offerEventToActiveXControlStatic (void* ptr) | |||
{ | |||
for (auto* ax : ActiveXHelpers::activeXComps) | |||
{ | |||
auto result = ax->offerEventToActiveXControl (ptr); | |||
if (result != S_FALSE) | |||
return result; | |||
} | |||
return S_FALSE; | |||
} | |||
LRESULT juce_offerEventToActiveXControl (::MSG& msg) | |||
{ | |||
if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST) | |||
return ActiveXControlComponent::offerEventToActiveXControlStatic (&msg); | |||
return S_FALSE; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,243 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
extern void* getUser32Function (const char*); | |||
namespace IconConverters | |||
{ | |||
extern HICON createHICONFromImage (const Image&, BOOL isIcon, int hotspotX, int hotspotY); | |||
} | |||
//============================================================================== | |||
class SystemTrayIconComponent::Pimpl | |||
{ | |||
public: | |||
Pimpl (SystemTrayIconComponent& owner_, HICON hicon, HWND hwnd) | |||
: owner (owner_), | |||
originalWndProc ((WNDPROC) GetWindowLongPtr (hwnd, GWLP_WNDPROC)), | |||
taskbarCreatedMessage (RegisterWindowMessage (TEXT ("TaskbarCreated"))) | |||
{ | |||
SetWindowLongPtr (hwnd, GWLP_WNDPROC, (LONG_PTR) hookedWndProc); | |||
zerostruct (iconData); | |||
iconData.cbSize = sizeof (iconData); | |||
iconData.hWnd = hwnd; | |||
iconData.uID = (UINT) (pointer_sized_int) hwnd; | |||
iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; | |||
iconData.uCallbackMessage = WM_TRAYNOTIFY; | |||
iconData.hIcon = hicon; | |||
notify (NIM_ADD); | |||
// In order to receive the "TaskbarCreated" message, we need to request that it's not filtered out. | |||
// (Need to load dynamically, as ChangeWindowMessageFilter is only available in Vista and later) | |||
typedef BOOL (WINAPI* ChangeWindowMessageFilterType) (UINT, DWORD); | |||
if (ChangeWindowMessageFilterType changeWindowMessageFilter | |||
= (ChangeWindowMessageFilterType) getUser32Function ("ChangeWindowMessageFilter")) | |||
changeWindowMessageFilter (taskbarCreatedMessage, 1 /* MSGFLT_ADD */); | |||
} | |||
~Pimpl() | |||
{ | |||
SetWindowLongPtr (iconData.hWnd, GWLP_WNDPROC, (LONG_PTR) originalWndProc); | |||
iconData.uFlags = 0; | |||
notify (NIM_DELETE); | |||
DestroyIcon (iconData.hIcon); | |||
} | |||
void updateIcon (HICON hicon) | |||
{ | |||
HICON oldIcon = iconData.hIcon; | |||
iconData.hIcon = hicon; | |||
iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; | |||
notify (NIM_MODIFY); | |||
DestroyIcon (oldIcon); | |||
} | |||
void setToolTip (const String& toolTip) | |||
{ | |||
iconData.uFlags = NIF_TIP; | |||
toolTip.copyToUTF16 (iconData.szTip, sizeof (iconData.szTip) - 1); | |||
notify (NIM_MODIFY); | |||
} | |||
void handleTaskBarEvent (const LPARAM lParam) | |||
{ | |||
if (owner.isCurrentlyBlockedByAnotherModalComponent()) | |||
{ | |||
if (lParam == WM_LBUTTONDOWN || lParam == WM_RBUTTONDOWN | |||
|| lParam == WM_LBUTTONDBLCLK || lParam == WM_RBUTTONDBLCLK) | |||
{ | |||
if (auto* current = Component::getCurrentlyModalComponent()) | |||
current->inputAttemptWhenModal(); | |||
} | |||
} | |||
else | |||
{ | |||
ModifierKeys eventMods (ModifierKeys::getCurrentModifiersRealtime()); | |||
if (lParam == WM_LBUTTONDOWN || lParam == WM_LBUTTONDBLCLK) | |||
eventMods = eventMods.withFlags (ModifierKeys::leftButtonModifier); | |||
else if (lParam == WM_RBUTTONDOWN || lParam == WM_RBUTTONDBLCLK) | |||
eventMods = eventMods.withFlags (ModifierKeys::rightButtonModifier); | |||
else if (lParam == WM_LBUTTONUP || lParam == WM_RBUTTONUP) | |||
eventMods = eventMods.withoutMouseButtons(); | |||
const Time eventTime (getMouseEventTime()); | |||
const MouseEvent e (Desktop::getInstance().getMainMouseSource(), {}, eventMods, | |||
MouseInputSource::invalidPressure, MouseInputSource::invalidOrientation, | |||
MouseInputSource::invalidRotation, MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, | |||
&owner, &owner, eventTime, {}, eventTime, 1, false); | |||
if (lParam == WM_LBUTTONDOWN || lParam == WM_RBUTTONDOWN) | |||
{ | |||
SetFocus (iconData.hWnd); | |||
SetForegroundWindow (iconData.hWnd); | |||
owner.mouseDown (e); | |||
} | |||
else if (lParam == WM_LBUTTONUP || lParam == WM_RBUTTONUP) | |||
{ | |||
owner.mouseUp (e); | |||
} | |||
else if (lParam == WM_LBUTTONDBLCLK || lParam == WM_RBUTTONDBLCLK) | |||
{ | |||
owner.mouseDoubleClick (e); | |||
} | |||
else if (lParam == WM_MOUSEMOVE) | |||
{ | |||
owner.mouseMove (e); | |||
} | |||
} | |||
} | |||
static Pimpl* getPimpl (HWND hwnd) | |||
{ | |||
if (JuceWindowIdentifier::isJUCEWindow (hwnd)) | |||
if (ComponentPeer* peer = (ComponentPeer*) GetWindowLongPtr (hwnd, 8)) | |||
if (SystemTrayIconComponent* const iconComp = dynamic_cast<SystemTrayIconComponent*> (&(peer->getComponent()))) | |||
return iconComp->pimpl; | |||
return nullptr; | |||
} | |||
static LRESULT CALLBACK hookedWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) | |||
{ | |||
if (Pimpl* const p = getPimpl (hwnd)) | |||
return p->windowProc (hwnd, message, wParam, lParam); | |||
return DefWindowProcW (hwnd, message, wParam, lParam); | |||
} | |||
LRESULT windowProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) | |||
{ | |||
if (message == WM_TRAYNOTIFY) | |||
{ | |||
handleTaskBarEvent (lParam); | |||
} | |||
else if (message == taskbarCreatedMessage) | |||
{ | |||
iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; | |||
notify (NIM_ADD); | |||
} | |||
return CallWindowProc (originalWndProc, hwnd, message, wParam, lParam); | |||
} | |||
void showBubble (const String& title, const String& content) | |||
{ | |||
iconData.uFlags = 0x10 /*NIF_INFO*/; | |||
title.copyToUTF16 (iconData.szInfoTitle, sizeof (iconData.szInfoTitle) - 1); | |||
content.copyToUTF16 (iconData.szInfo, sizeof (iconData.szInfo) - 1); | |||
notify (NIM_MODIFY); | |||
} | |||
SystemTrayIconComponent& owner; | |||
NOTIFYICONDATA iconData; | |||
private: | |||
WNDPROC originalWndProc; | |||
const DWORD taskbarCreatedMessage; | |||
enum { WM_TRAYNOTIFY = WM_USER + 100 }; | |||
void notify (DWORD message) noexcept { Shell_NotifyIcon (message, &iconData); } | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | |||
}; | |||
//============================================================================== | |||
void SystemTrayIconComponent::setIconImage (const Image& newImage) | |||
{ | |||
if (newImage.isValid()) | |||
{ | |||
HICON hicon = IconConverters::createHICONFromImage (newImage, TRUE, 0, 0); | |||
if (pimpl == nullptr) | |||
pimpl = new Pimpl (*this, hicon, (HWND) getWindowHandle()); | |||
else | |||
pimpl->updateIcon (hicon); | |||
} | |||
else | |||
{ | |||
pimpl = nullptr; | |||
} | |||
} | |||
void SystemTrayIconComponent::setIconTooltip (const String& tooltip) | |||
{ | |||
if (pimpl != nullptr) | |||
pimpl->setToolTip (tooltip); | |||
} | |||
void SystemTrayIconComponent::setHighlighted (bool) | |||
{ | |||
// N/A on Windows. | |||
} | |||
void SystemTrayIconComponent::showInfoBubble (const String& title, const String& content) | |||
{ | |||
if (pimpl != nullptr) | |||
pimpl->showBubble (title, content); | |||
} | |||
void SystemTrayIconComponent::hideInfoBubble() | |||
{ | |||
showInfoBubble (String(), String()); | |||
} | |||
void* SystemTrayIconComponent::getNativeHandle() const | |||
{ | |||
return pimpl != nullptr ? &(pimpl->iconData) : nullptr; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,439 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License | |||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the | |||
27th April 2017). | |||
End User License Agreement: www.juce.com/juce-5-licence | |||
Privacy Policy: www.juce.com/juce-5-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
JUCE_DECLARE_UUID_GETTER (DWebBrowserEvents2, "34A715A0-6587-11D0-924A-0020AFC7AC4D") | |||
JUCE_DECLARE_UUID_GETTER (IConnectionPointContainer, "B196B284-BAB4-101A-B69C-00AA00341D07") | |||
JUCE_DECLARE_UUID_GETTER (IWebBrowser2, "D30C1661-CDAF-11D0-8A3E-00C04FC9E26E") | |||
#if JUCE_MINGW | |||
#define DISPID_NAVIGATEERROR 271 | |||
class WebBrowser; | |||
#endif | |||
JUCE_DECLARE_UUID_GETTER (WebBrowser, "8856F961-340A-11D0-A96B-00C04FD705A2") | |||
class WebBrowserComponent::Pimpl : public ActiveXControlComponent | |||
{ | |||
public: | |||
Pimpl() {} | |||
~Pimpl() | |||
{ | |||
if (connectionPoint != nullptr) | |||
connectionPoint->Unadvise (adviseCookie); | |||
if (browser != nullptr) | |||
browser->Release(); | |||
} | |||
void createBrowser() | |||
{ | |||
auto webCLSID = __uuidof (WebBrowser); | |||
createControl (&webCLSID); | |||
auto iidWebBrowser2 = __uuidof (IWebBrowser2); | |||
auto iidConnectionPointContainer = __uuidof (IConnectionPointContainer); | |||
browser = (IWebBrowser2*) queryInterface (&iidWebBrowser2); | |||
if (auto connectionPointContainer = (IConnectionPointContainer*) queryInterface (&iidConnectionPointContainer)) | |||
{ | |||
connectionPointContainer->FindConnectionPoint (__uuidof (DWebBrowserEvents2), &connectionPoint); | |||
if (connectionPoint != nullptr) | |||
{ | |||
auto* owner = dynamic_cast<WebBrowserComponent*> (getParentComponent()); | |||
jassert (owner != nullptr); | |||
auto handler = new EventHandler (*owner); | |||
connectionPoint->Advise (handler, &adviseCookie); | |||
handler->Release(); | |||
} | |||
} | |||
} | |||
void goToURL (const String& url, | |||
const StringArray* headers, | |||
const MemoryBlock* postData) | |||
{ | |||
if (browser != nullptr) | |||
{ | |||
LPSAFEARRAY sa = nullptr; | |||
VARIANT headerFlags, frame, postDataVar, headersVar; // (_variant_t isn't available in all compilers) | |||
VariantInit (&headerFlags); | |||
VariantInit (&frame); | |||
VariantInit (&postDataVar); | |||
VariantInit (&headersVar); | |||
if (headers != nullptr) | |||
{ | |||
V_VT (&headersVar) = VT_BSTR; | |||
V_BSTR (&headersVar) = SysAllocString ((const OLECHAR*) headers->joinIntoString ("\r\n").toWideCharPointer()); | |||
} | |||
if (postData != nullptr && postData->getSize() > 0) | |||
{ | |||
sa = SafeArrayCreateVector (VT_UI1, 0, (ULONG) postData->getSize()); | |||
if (sa != nullptr) | |||
{ | |||
void* data = nullptr; | |||
SafeArrayAccessData (sa, &data); | |||
jassert (data != nullptr); | |||
if (data != nullptr) | |||
{ | |||
postData->copyTo (data, 0, postData->getSize()); | |||
SafeArrayUnaccessData (sa); | |||
VARIANT postDataVar2; | |||
VariantInit (&postDataVar2); | |||
V_VT (&postDataVar2) = VT_ARRAY | VT_UI1; | |||
V_ARRAY (&postDataVar2) = sa; | |||
postDataVar = postDataVar2; | |||
} | |||
} | |||
} | |||
auto urlBSTR = SysAllocString ((const OLECHAR*) url.toWideCharPointer()); | |||
browser->Navigate (urlBSTR, &headerFlags, &frame, &postDataVar, &headersVar); | |||
SysFreeString (urlBSTR); | |||
if (sa != nullptr) | |||
SafeArrayDestroy (sa); | |||
VariantClear (&headerFlags); | |||
VariantClear (&frame); | |||
VariantClear (&postDataVar); | |||
VariantClear (&headersVar); | |||
} | |||
} | |||
//============================================================================== | |||
IWebBrowser2* browser = nullptr; | |||
private: | |||
IConnectionPoint* connectionPoint = nullptr; | |||
DWORD adviseCookie = 0; | |||
//============================================================================== | |||
struct EventHandler : public ComBaseClassHelper<IDispatch>, | |||
public ComponentMovementWatcher | |||
{ | |||
EventHandler (WebBrowserComponent& w) : ComponentMovementWatcher (&w), owner (w) {} | |||
JUCE_COMRESULT GetTypeInfoCount (UINT*) { return E_NOTIMPL; } | |||
JUCE_COMRESULT GetTypeInfo (UINT, LCID, ITypeInfo**) { return E_NOTIMPL; } | |||
JUCE_COMRESULT GetIDsOfNames (REFIID, LPOLESTR*, UINT, LCID, DISPID*) { return E_NOTIMPL; } | |||
JUCE_COMRESULT Invoke (DISPID dispIdMember, REFIID /*riid*/, LCID /*lcid*/, WORD /*wFlags*/, DISPPARAMS* pDispParams, | |||
VARIANT* /*pVarResult*/, EXCEPINFO* /*pExcepInfo*/, UINT* /*puArgErr*/) | |||
{ | |||
if (dispIdMember == DISPID_BEFORENAVIGATE2) | |||
{ | |||
*pDispParams->rgvarg->pboolVal | |||
= owner.pageAboutToLoad (getStringFromVariant (pDispParams->rgvarg[5].pvarVal)) ? VARIANT_FALSE | |||
: VARIANT_TRUE; | |||
return S_OK; | |||
} | |||
if (dispIdMember == 273 /*DISPID_NEWWINDOW3*/) | |||
{ | |||
owner.newWindowAttemptingToLoad (pDispParams->rgvarg[0].bstrVal); | |||
*pDispParams->rgvarg[3].pboolVal = VARIANT_TRUE; | |||
return S_OK; | |||
} | |||
if (dispIdMember == DISPID_DOCUMENTCOMPLETE) | |||
{ | |||
owner.pageFinishedLoading (getStringFromVariant (pDispParams->rgvarg[0].pvarVal)); | |||
return S_OK; | |||
} | |||
if (dispIdMember == DISPID_NAVIGATEERROR) | |||
{ | |||
int statusCode = pDispParams->rgvarg[1].pvarVal->intVal; | |||
*pDispParams->rgvarg[0].pboolVal = VARIANT_FALSE; | |||
// IWebBrowser2 also reports http status codes here, we need | |||
// report only network erros | |||
if (statusCode < 0) | |||
{ | |||
LPTSTR messageBuffer = nullptr; | |||
auto size = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, | |||
nullptr, statusCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), | |||
(LPTSTR) &messageBuffer, 0, nullptr); | |||
String message (messageBuffer, size); | |||
LocalFree (messageBuffer); | |||
if (! owner.pageLoadHadNetworkError (message)) | |||
*pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE; | |||
} | |||
return S_OK; | |||
} | |||
if (dispIdMember == 263 /*DISPID_WINDOWCLOSING*/) | |||
{ | |||
owner.windowCloseRequest(); | |||
// setting this bool tells the browser to ignore the event - we'll handle it. | |||
if (pDispParams->cArgs > 0 && pDispParams->rgvarg[0].vt == (VT_BYREF | VT_BOOL)) | |||
*pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE; | |||
return S_OK; | |||
} | |||
return E_NOTIMPL; | |||
} | |||
void componentMovedOrResized (bool, bool) override {} | |||
void componentPeerChanged() override {} | |||
void componentVisibilityChanged() override { owner.visibilityChanged(); } | |||
private: | |||
WebBrowserComponent& owner; | |||
static String getStringFromVariant (VARIANT* v) | |||
{ | |||
return (v->vt & VT_BYREF) != 0 ? *v->pbstrVal | |||
: v->bstrVal; | |||
} | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EventHandler) | |||
}; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | |||
}; | |||
//============================================================================== | |||
WebBrowserComponent::WebBrowserComponent (const bool unloadPageWhenBrowserIsHidden_) | |||
: browser (nullptr), | |||
blankPageShown (false), | |||
unloadPageWhenBrowserIsHidden (unloadPageWhenBrowserIsHidden_) | |||
{ | |||
setOpaque (true); | |||
addAndMakeVisible (browser = new Pimpl()); | |||
} | |||
WebBrowserComponent::~WebBrowserComponent() | |||
{ | |||
delete browser; | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::goToURL (const String& url, | |||
const StringArray* headers, | |||
const MemoryBlock* postData) | |||
{ | |||
lastURL = url; | |||
if (headers != nullptr) | |||
lastHeaders = *headers; | |||
else | |||
lastHeaders.clear(); | |||
if (postData != nullptr) | |||
lastPostData = *postData; | |||
else | |||
lastPostData.reset(); | |||
blankPageShown = false; | |||
if (browser->browser == nullptr) | |||
checkWindowAssociation(); | |||
browser->goToURL (url, headers, postData); | |||
} | |||
void WebBrowserComponent::stop() | |||
{ | |||
if (browser->browser != nullptr) | |||
browser->browser->Stop(); | |||
} | |||
void WebBrowserComponent::goBack() | |||
{ | |||
lastURL.clear(); | |||
blankPageShown = false; | |||
if (browser->browser != nullptr) | |||
browser->browser->GoBack(); | |||
} | |||
void WebBrowserComponent::goForward() | |||
{ | |||
lastURL.clear(); | |||
if (browser->browser != nullptr) | |||
browser->browser->GoForward(); | |||
} | |||
void WebBrowserComponent::refresh() | |||
{ | |||
if (browser->browser != nullptr) | |||
browser->browser->Refresh(); | |||
} | |||
//============================================================================== | |||
void WebBrowserComponent::paint (Graphics& g) | |||
{ | |||
if (browser->browser == nullptr) | |||
{ | |||
g.fillAll (Colours::white); | |||
checkWindowAssociation(); | |||
} | |||
} | |||
void WebBrowserComponent::checkWindowAssociation() | |||
{ | |||
if (isShowing()) | |||
{ | |||
if (browser->browser == nullptr && getPeer() != nullptr) | |||
{ | |||
browser->createBrowser(); | |||
reloadLastURL(); | |||
} | |||
else | |||
{ | |||
if (blankPageShown) | |||
goBack(); | |||
} | |||
} | |||
else | |||
{ | |||
if (browser != nullptr && unloadPageWhenBrowserIsHidden && ! blankPageShown) | |||
{ | |||
// when the component becomes invisible, some stuff like flash | |||
// carries on playing audio, so we need to force it onto a blank | |||
// page to avoid this.. | |||
blankPageShown = true; | |||
browser->goToURL ("about:blank", 0, 0); | |||
} | |||
} | |||
} | |||
void WebBrowserComponent::reloadLastURL() | |||
{ | |||
if (lastURL.isNotEmpty()) | |||
{ | |||
goToURL (lastURL, &lastHeaders, &lastPostData); | |||
lastURL.clear(); | |||
} | |||
} | |||
void WebBrowserComponent::parentHierarchyChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::resized() | |||
{ | |||
browser->setSize (getWidth(), getHeight()); | |||
} | |||
void WebBrowserComponent::visibilityChanged() | |||
{ | |||
checkWindowAssociation(); | |||
} | |||
void WebBrowserComponent::focusGained (FocusChangeType) | |||
{ | |||
auto iidOleObject = __uuidof (IOleObject); | |||
auto iidOleWindow = __uuidof (IOleWindow); | |||
if (auto oleObject = (IOleObject*) browser->queryInterface (&iidOleObject)) | |||
{ | |||
if (auto oleWindow = (IOleWindow*) browser->queryInterface (&iidOleWindow)) | |||
{ | |||
IOleClientSite* oleClientSite = nullptr; | |||
if (SUCCEEDED (oleObject->GetClientSite (&oleClientSite))) | |||
{ | |||
HWND hwnd; | |||
oleWindow->GetWindow (&hwnd); | |||
oleObject->DoVerb (OLEIVERB_UIACTIVATE, nullptr, oleClientSite, 0, hwnd, nullptr); | |||
oleClientSite->Release(); | |||
} | |||
oleWindow->Release(); | |||
} | |||
oleObject->Release(); | |||
} | |||
} | |||
void WebBrowserComponent::clearCookies() | |||
{ | |||
HeapBlock<::INTERNET_CACHE_ENTRY_INFO> entry; | |||
::DWORD entrySize = sizeof (::INTERNET_CACHE_ENTRY_INFO); | |||
#if JUCE_MINGW | |||
const auto searchPattern = "cookie:"; | |||
#else | |||
const auto searchPattern = TEXT ("cookie:"); | |||
#endif | |||
::HANDLE urlCacheHandle = ::FindFirstUrlCacheEntry (searchPattern, entry.getData(), &entrySize); | |||
if (urlCacheHandle == nullptr && GetLastError() == ERROR_INSUFFICIENT_BUFFER) | |||
{ | |||
entry.realloc (1, entrySize); | |||
urlCacheHandle = ::FindFirstUrlCacheEntry (searchPattern, entry.getData(), &entrySize); | |||
} | |||
if (urlCacheHandle != nullptr) | |||
{ | |||
for (;;) | |||
{ | |||
::DeleteUrlCacheEntry (entry.getData()->lpszSourceUrlName); | |||
if (::FindNextUrlCacheEntry (urlCacheHandle, entry.getData(), &entrySize) == 0) | |||
{ | |||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) | |||
{ | |||
entry.realloc (1, entrySize); | |||
if (::FindNextUrlCacheEntry (urlCacheHandle, entry.getData(), &entrySize) != 0) | |||
continue; | |||
} | |||
break; | |||
} | |||
} | |||
FindCloseUrlCache (urlCacheHandle); | |||
} | |||
} | |||
} // namespace juce |