@@ -59,7 +59,7 @@ static void doBasicProjectSetup (Project& project, const NewProjectTemplates::Pr | |||
project.getProjectValue (Ids::useAppConfig) = false; | |||
project.getProjectValue (Ids::addUsingNamespaceToJuceHeader) = false; | |||
if (! ProjucerApplication::getApp().getLicenseController().getCurrentState().isPaidOrGPL()) | |||
if (! ProjucerApplication::getApp().getLicenseController().getCurrentState().canUnlockFullFeatures()) | |||
project.getProjectValue (Ids::displaySplashScreen) = true; | |||
if (NewProjectTemplates::isPlugin (projectTemplate)) | |||
@@ -19,14 +19,23 @@ | |||
#pragma once | |||
#include "jucer_LicenseState.h" | |||
#include "jucer_LicenseQueryThread.h" | |||
//============================================================================== | |||
class LicenseController | |||
class LicenseController : private Timer | |||
{ | |||
public: | |||
LicenseController() = default; | |||
LicenseController() | |||
{ | |||
checkLicense(); | |||
} | |||
//============================================================================== | |||
static LicenseState getGPLState() | |||
{ | |||
return { LicenseState::Type::gpl, projucerMajorVersion, {}, {} }; | |||
} | |||
LicenseState getCurrentState() const noexcept | |||
{ | |||
return state; | |||
@@ -34,10 +43,13 @@ public: | |||
void setState (const LicenseState& newState) | |||
{ | |||
state = newState; | |||
licenseStateToSettings (state, getGlobalProperties()); | |||
if (state != newState) | |||
{ | |||
state = newState; | |||
licenseStateToSettings (state, getGlobalProperties()); | |||
stateListeners.call ([] (LicenseStateListener& l) { l.licenseStateChanged(); }); | |||
stateListeners.call ([] (LicenseStateListener& l) { l.licenseStateChanged(); }); | |||
} | |||
} | |||
void resetState() | |||
@@ -45,26 +57,21 @@ public: | |||
setState ({}); | |||
} | |||
static LicenseState getGPLState() | |||
void signIn (const String& email, const String& password, | |||
std::function<void (const String&)> completionCallback) | |||
{ | |||
static auto logoImage = []() -> Image | |||
{ | |||
if (auto logo = Drawable::createFromImageData (BinaryData::gpl_logo_svg, BinaryData::gpl_logo_svgSize)) | |||
{ | |||
auto bounds = logo->getDrawableBounds(); | |||
Image image (Image::ARGB, roundToInt (bounds.getWidth()), roundToInt (bounds.getHeight()), true); | |||
Graphics g (image); | |||
logo->draw (g, 1.0f); | |||
return image; | |||
} | |||
jassertfalse; | |||
return {}; | |||
}(); | |||
licenseQueryThread.doSignIn (email, password, | |||
[this, completionCallback] (LicenseQueryThread::ErrorMessageAndType error, | |||
LicenseState newState) | |||
{ | |||
completionCallback (error.first); | |||
setState (newState); | |||
}); | |||
} | |||
return { LicenseState::Type::gpl, {}, {}, logoImage }; | |||
void cancelSignIn() | |||
{ | |||
licenseQueryThread.cancelRunningJobs(); | |||
} | |||
//============================================================================== | |||
@@ -112,24 +119,6 @@ private: | |||
return LicenseState::Type::none; | |||
} | |||
static Image avatarFromLicenseState (const String& licenseState) | |||
{ | |||
MemoryOutputStream imageData; | |||
Base64::convertFromBase64 (imageData, licenseState); | |||
return ImageFileFormat::loadFrom (imageData.getData(), imageData.getDataSize()); | |||
} | |||
static String avatarToLicenseState (Image avatarImage) | |||
{ | |||
MemoryOutputStream imageData; | |||
if (avatarImage.isValid() && PNGImageFormat().writeImageToStream (avatarImage, imageData)) | |||
return Base64::toBase64 (imageData.getData(), imageData.getDataSize()); | |||
return {}; | |||
} | |||
static LicenseState licenseStateFromSettings (PropertiesFile& props) | |||
{ | |||
if (auto licenseXml = props.getXmlValue ("license")) | |||
@@ -140,9 +129,9 @@ private: | |||
auto stateFromOldSettings = [&licenseXml]() -> LicenseState | |||
{ | |||
return { getLicenseTypeFromValue (licenseXml->getChildElementAllSubText ("type", {})), | |||
licenseXml->getChildElementAllSubText ("authToken", {}), | |||
licenseXml->getChildElementAllSubText ("version", "-1").getIntValue(), | |||
licenseXml->getChildElementAllSubText ("username", {}), | |||
avatarFromLicenseState (licenseXml->getStringAttribute ("avatar", {})) }; | |||
licenseXml->getChildElementAllSubText ("authToken", {}) }; | |||
}(); | |||
licenseStateToSettings (stateFromOldSettings, props); | |||
@@ -151,9 +140,9 @@ private: | |||
} | |||
return { getLicenseTypeFromValue (licenseXml->getStringAttribute ("type", {})), | |||
licenseXml->getStringAttribute ("authToken", {}), | |||
licenseXml->getIntAttribute ("version", -1), | |||
licenseXml->getStringAttribute ("username", {}), | |||
avatarFromLicenseState (licenseXml->getStringAttribute ("avatar", {})) }; | |||
licenseXml->getStringAttribute ("authToken", {}) }; | |||
} | |||
return {}; | |||
@@ -163,16 +152,16 @@ private: | |||
{ | |||
props.removeValue ("license"); | |||
if (state.isValid()) | |||
if (state.isSignedIn()) | |||
{ | |||
XmlElement licenseXml ("license"); | |||
if (auto* typeString = getLicenseStateValue (state.type)) | |||
licenseXml.setAttribute ("type", typeString); | |||
licenseXml.setAttribute ("authToken", state.authToken); | |||
licenseXml.setAttribute ("version", state.version); | |||
licenseXml.setAttribute ("username", state.username); | |||
licenseXml.setAttribute ("avatar", avatarToLicenseState (state.avatar)); | |||
licenseXml.setAttribute ("authToken", state.authToken); | |||
props.setValue ("license", &licenseXml); | |||
} | |||
@@ -180,6 +169,38 @@ private: | |||
props.saveIfNeeded(); | |||
} | |||
//============================================================================== | |||
void checkLicense() | |||
{ | |||
if (state.isSignedIn() && ! state.isGPL()) | |||
{ | |||
auto completionCallback = [this] (LicenseQueryThread::ErrorMessageAndType error, | |||
LicenseState updatedState) | |||
{ | |||
if (error == LicenseQueryThread::ErrorMessageAndType()) | |||
{ | |||
setState (updatedState); | |||
} | |||
else if ((error.second == LicenseQueryThread::ErrorType::busy | |||
|| error.second == LicenseQueryThread::ErrorType::cancelled | |||
|| error.second == LicenseQueryThread::ErrorType::connectionError) | |||
&& ! hasRetriedLicenseCheck) | |||
{ | |||
hasRetriedLicenseCheck = true; | |||
startTimer (10000); | |||
} | |||
}; | |||
licenseQueryThread.checkLicenseValidity (state, std::move (completionCallback)); | |||
} | |||
} | |||
void timerCallback() override | |||
{ | |||
stopTimer(); | |||
checkLicense(); | |||
} | |||
//============================================================================== | |||
#if JUCER_ENABLE_GPL_MODE | |||
LicenseState state = getGPLState(); | |||
@@ -188,6 +209,8 @@ private: | |||
#endif | |||
ListenerList<LicenseStateListener> stateListeners; | |||
LicenseQueryThread licenseQueryThread; | |||
bool hasRetriedLicenseCheck = false; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseController) | |||
@@ -18,47 +18,147 @@ | |||
#pragma once | |||
//============================================================================== | |||
class LicenseQueryThread : public Thread | |||
namespace LicenseHelpers | |||
{ | |||
public: | |||
LicenseQueryThread (const String& userEmail, const String& userPassword, | |||
std::function<void (LicenseState, String)>&& cb) | |||
: Thread ("LicenseQueryThread"), | |||
email (userEmail), | |||
password (userPassword), | |||
completionCallback (std::move (cb)) | |||
inline LicenseState::Type licenseTypeForString (const String& licenseString) | |||
{ | |||
startThread(); | |||
if (licenseString == "juce-pro") return LicenseState::Type::pro; | |||
if (licenseString == "juce-indie") return LicenseState::Type::indie; | |||
if (licenseString == "juce-edu") return LicenseState::Type::educational; | |||
if (licenseString == "juce-personal") return LicenseState::Type::personal; | |||
jassertfalse; // unknown type | |||
return LicenseState::Type::none; | |||
} | |||
~LicenseQueryThread() override | |||
using LicenseVersionAndType = std::pair<int, LicenseState::Type>; | |||
inline LicenseVersionAndType findBestLicense (std::vector<LicenseVersionAndType>&& licenses) | |||
{ | |||
signalThreadShouldExit(); | |||
waitForThreadToExit (6000); | |||
if (licenses.size() == 1) | |||
return licenses[0]; | |||
auto getValueForLicenceType = [] (LicenseState::Type type) | |||
{ | |||
switch (type) | |||
{ | |||
case LicenseState::Type::pro: return 4; | |||
case LicenseState::Type::indie: return 3; | |||
case LicenseState::Type::educational: return 2; | |||
case LicenseState::Type::personal: return 1; | |||
case LicenseState::Type::gpl: | |||
case LicenseState::Type::none: | |||
default: return -1; | |||
} | |||
}; | |||
std::sort (licenses.begin(), licenses.end(), | |||
[getValueForLicenceType] (const LicenseVersionAndType& l1, const LicenseVersionAndType& l2) | |||
{ | |||
if (l1.first > l2.first) | |||
return true; | |||
if (l1.first == l2.first) | |||
return getValueForLicenceType (l1.second) > getValueForLicenceType (l2.second); | |||
return false; | |||
}); | |||
auto findFirstLicense = [&licenses] (bool isPaid) | |||
{ | |||
auto iter = std::find_if (licenses.begin(), licenses.end(), | |||
[isPaid] (const LicenseVersionAndType& l) | |||
{ | |||
auto proOrIndie = (l.second == LicenseState::Type::pro || l.second == LicenseState::Type::indie); | |||
return isPaid ? proOrIndie : ! proOrIndie; | |||
}); | |||
return iter != licenses.end() ? *iter | |||
: LicenseVersionAndType(); | |||
}; | |||
auto newestPaid = findFirstLicense (true); | |||
auto newestFree = findFirstLicense (false); | |||
if (newestPaid.first >= projucerMajorVersion || newestPaid.first >= newestFree.first) | |||
return newestPaid; | |||
return newestFree; | |||
} | |||
} | |||
//============================================================================== | |||
class LicenseQueryThread | |||
{ | |||
public: | |||
enum class ErrorType | |||
{ | |||
busy, | |||
cancelled, | |||
connectionError, | |||
webResponseError | |||
}; | |||
using ErrorMessageAndType = std::pair<String, ErrorType>; | |||
using LicenseQueryCallback = std::function<void (ErrorMessageAndType, LicenseState)>; | |||
//============================================================================== | |||
LicenseQueryThread() = default; | |||
void run() override | |||
void checkLicenseValidity (const LicenseState& state, LicenseQueryCallback completionCallback) | |||
{ | |||
LicenseState state; | |||
if (jobPool.getNumJobs() > 0) | |||
{ | |||
completionCallback ({ {}, ErrorType::busy }, {}); | |||
return; | |||
} | |||
auto errorMessage = runJob (std::make_unique<UserLogin> (email, password), state); | |||
jobPool.addJob ([this, state, completionCallback] | |||
{ | |||
auto updatedState = state; | |||
if (errorMessage.isEmpty()) | |||
errorMessage = runJob (std::make_unique<UserLicenseQuery> (state.authToken), state); | |||
auto result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), updatedState); | |||
if (errorMessage.isNotEmpty()) | |||
state = {}; | |||
WeakReference<LicenseQueryThread> weakThis (this); | |||
MessageManager::callAsync ([weakThis, result, updatedState, completionCallback] | |||
{ | |||
if (weakThis != nullptr) | |||
completionCallback (result, updatedState); | |||
}); | |||
}); | |||
} | |||
WeakReference<LicenseQueryThread> weakThis (this); | |||
MessageManager::callAsync ([this, weakThis, state, errorMessage] | |||
void doSignIn (const String& email, const String& password, LicenseQueryCallback completionCallback) | |||
{ | |||
cancelRunningJobs(); | |||
jobPool.addJob ([this, email, password, completionCallback] | |||
{ | |||
if (weakThis != nullptr) | |||
completionCallback (state, errorMessage); | |||
LicenseState state; | |||
auto result = runTask (std::make_unique<UserLogin> (email, password), state); | |||
if (result == ErrorMessageAndType()) | |||
result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), state); | |||
if (result != ErrorMessageAndType()) | |||
state = {}; | |||
WeakReference<LicenseQueryThread> weakThis (this); | |||
MessageManager::callAsync ([weakThis, result, state, completionCallback] | |||
{ | |||
if (weakThis != nullptr) | |||
completionCallback (result, state); | |||
}); | |||
}); | |||
} | |||
void cancelRunningJobs() | |||
{ | |||
jobPool.removeAllJobs (true, 500); | |||
} | |||
private: | |||
//============================================================================== | |||
struct AccountEnquiryBase | |||
@@ -115,22 +215,7 @@ private: | |||
auto json = JSON::parse (serverResponse); | |||
licenseState.authToken = json.getProperty ("token", {}).toString(); | |||
licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString(); | |||
auto avatarURL = json.getProperty ("user", {}).getProperty ("avatar_url", {}).toString(); | |||
if (avatarURL.isNotEmpty()) | |||
{ | |||
URL url (avatarURL); | |||
if (auto stream = url.createInputStream (false, nullptr, nullptr, {}, 5000)) | |||
{ | |||
MemoryBlock mb; | |||
stream->readIntoMemoryBlock (mb); | |||
licenseState.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize()); | |||
} | |||
} | |||
licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString(); | |||
return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty()); | |||
} | |||
@@ -174,30 +259,27 @@ private: | |||
if (auto* licensesJson = json.getArray()) | |||
{ | |||
StringArray licenseTypes; | |||
std::vector<LicenseHelpers::LicenseVersionAndType> licenses; | |||
for (auto& license : *licensesJson) | |||
{ | |||
auto status = license.getProperty ("status", {}).toString(); | |||
auto version = license.getProperty ("product_version", {}).toString().trim(); | |||
auto type = license.getProperty ("licence_type", {}).toString(); | |||
auto status = license.getProperty ("status", {}).toString(); | |||
if (status == "active") | |||
licenseTypes.add (license.getProperty ("licence_type", {}).toString()); | |||
if (status == "active" && type.isNotEmpty() && version.isNotEmpty()) | |||
licenses.push_back ({ version.getIntValue(), LicenseHelpers::licenseTypeForString (type) }); | |||
} | |||
licenseTypes.removeEmptyStrings(); | |||
licenseTypes.removeDuplicates (false); | |||
licenseState.type = [licenseTypes]() | |||
if (! licenses.empty()) | |||
{ | |||
if (licenseTypes.contains ("juce-pro")) return LicenseState::Type::pro; | |||
else if (licenseTypes.contains ("juce-indie")) return LicenseState::Type::indie; | |||
else if (licenseTypes.contains ("juce-personal")) return LicenseState::Type::personal; | |||
else if (licenseTypes.contains ("juce-edu")) return LicenseState::Type::educational; | |||
auto bestLicense = LicenseHelpers::findBestLicense (std::move (licenses)); | |||
return LicenseState::Type::none; | |||
}(); | |||
licenseState.version = bestLicense.first; | |||
licenseState.type = bestLicense.second; | |||
} | |||
return (licenseState.type != LicenseState::Type::none); | |||
return true; | |||
} | |||
return false; | |||
@@ -217,33 +299,34 @@ private: | |||
return JSON::toString (var (d.get())); | |||
} | |||
String runJob (std::unique_ptr<AccountEnquiryBase> accountEnquiryJob, LicenseState& state) | |||
static ErrorMessageAndType runTask (std::unique_ptr<AccountEnquiryBase> accountEnquiryTask, LicenseState& state) | |||
{ | |||
const ErrorMessageAndType cancelledError ("Cancelled.", ErrorType::cancelled); | |||
const String endpointURL = "https://api.juce.com/api/v1"; | |||
auto url = URL (endpointURL + accountEnquiryJob->getEndpointURLSuffix()); | |||
auto url = URL (endpointURL + accountEnquiryTask->getEndpointURLSuffix()); | |||
auto isPOST = accountEnquiryJob->isPOSTLikeRequest(); | |||
auto isPOST = accountEnquiryTask->isPOSTLikeRequest(); | |||
if (isPOST) | |||
url = url.withPOSTData (postDataStringAsJSON (accountEnquiryJob->getParameterNamesAndValues())); | |||
url = url.withPOSTData (postDataStringAsJSON (accountEnquiryTask->getParameterNamesAndValues())); | |||
if (threadShouldExit()) | |||
return "Cancelled."; | |||
if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
return cancelledError; | |||
int statusCode = 0; | |||
auto urlStream = url.createInputStream (isPOST, nullptr, nullptr, | |||
accountEnquiryJob->getExtraHeaders(), | |||
accountEnquiryTask->getExtraHeaders(), | |||
5000, nullptr, &statusCode); | |||
if (urlStream == nullptr) | |||
return "Failed to connect to the web server."; | |||
return { "Failed to connect to the web server.", ErrorType::connectionError }; | |||
if (statusCode != accountEnquiryJob->getSuccessCode()) | |||
return accountEnquiryJob->errorCodeToString (statusCode); | |||
if (statusCode != accountEnquiryTask->getSuccessCode()) | |||
return { accountEnquiryTask->errorCodeToString (statusCode), ErrorType::webResponseError }; | |||
if (threadShouldExit()) | |||
return "Cancelled."; | |||
if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
return cancelledError; | |||
String response; | |||
@@ -252,8 +335,8 @@ private: | |||
char buffer [8192]; | |||
auto num = urlStream->read (buffer, sizeof (buffer)); | |||
if (threadShouldExit()) | |||
return "Cancelled."; | |||
if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
return cancelledError; | |||
if (num <= 0) | |||
break; | |||
@@ -261,18 +344,17 @@ private: | |||
response += buffer; | |||
} | |||
if (threadShouldExit()) | |||
return "Cancelled."; | |||
if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) | |||
return cancelledError; | |||
if (! accountEnquiryJob->parseServerResponse (response, state)) | |||
return "Failed to parse server response."; | |||
if (! accountEnquiryTask->parseServerResponse (response, state)) | |||
return { "Failed to parse server response.", ErrorType::webResponseError }; | |||
return {}; | |||
} | |||
//============================================================================== | |||
const String email, password; | |||
const std::function<void (LicenseState, String)> completionCallback; | |||
ThreadPool jobPool { 1 }; | |||
//============================================================================== | |||
JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread) | |||
@@ -34,16 +34,32 @@ struct LicenseState | |||
LicenseState() = default; | |||
LicenseState (Type t, String token, String user, Image avatarImage) | |||
: type (t), authToken (token), username (user), avatar (avatarImage) | |||
LicenseState (Type t, int v, String user, String token) | |||
: type (t), version (v), username (user), authToken (token) | |||
{ | |||
} | |||
bool isValid() const noexcept { return isGPL() || (type != Type::none && authToken.isNotEmpty() && username.isNotEmpty()); } | |||
bool operator== (const LicenseState& other) const noexcept | |||
{ | |||
return type == other.type | |||
&& version == other.version | |||
&& username == other.username | |||
&& authToken == other.authToken; | |||
} | |||
bool operator != (const LicenseState& other) const noexcept | |||
{ | |||
return ! operator== (other); | |||
} | |||
bool isSignedIn() const noexcept { return isGPL() || (version > 0 && username.isNotEmpty()); } | |||
bool isOldLicense() const noexcept { return isSignedIn() && version < projucerMajorVersion; } | |||
bool isGPL() const noexcept { return type == Type::gpl; } | |||
bool isPaid() const noexcept { return type == Type::indie || type == Type::pro; } | |||
bool isGPL() const noexcept { return type == Type::gpl; } | |||
bool isPaidOrGPL() const noexcept { return isPaid() || isGPL(); } | |||
bool canUnlockFullFeatures() const noexcept | |||
{ | |||
return isGPL() || (isSignedIn() && ! isOldLicense() && (type == Type::indie || type == Type::pro)); | |||
} | |||
String getLicenseTypeString() const | |||
{ | |||
@@ -63,6 +79,6 @@ struct LicenseState | |||
} | |||
Type type = Type::none; | |||
String authToken, username; | |||
Image avatar; | |||
int version = -1; | |||
String username, authToken; | |||
}; |
@@ -18,7 +18,7 @@ | |||
#pragma once | |||
#include "jucer_LicenseQueryThread.h" | |||
#include "../../Project/UI/jucer_UserAvatarComponent.h" | |||
//============================================================================== | |||
@@ -74,6 +74,11 @@ public: | |||
setSize (300, 350); | |||
} | |||
~LoginFormComponent() override | |||
{ | |||
ProjucerApplication::getApp().getLicenseController().cancelSignIn(); | |||
} | |||
void resized() override | |||
{ | |||
auto bounds = getLocalBounds().reduced (20); | |||
@@ -185,9 +190,6 @@ private: | |||
void submitDetails() | |||
{ | |||
if ((licenseQueryThread != nullptr && licenseQueryThread->isThreadRunning())) | |||
return; | |||
auto loginFormError = checkLoginFormsAreValid(); | |||
if (loginFormError.isNotEmpty()) | |||
@@ -199,29 +201,27 @@ private: | |||
updateLoginButtonStates (true); | |||
WeakReference<Component> weakThis (this); | |||
licenseQueryThread.reset (new LicenseQueryThread (emailBox.getText(), passwordBox.getText(), | |||
[this, weakThis] (LicenseState newState, String errorMessage) | |||
{ | |||
if (weakThis == nullptr) | |||
return; | |||
updateLoginButtonStates (false); | |||
if (errorMessage.isNotEmpty()) | |||
{ | |||
showErrorMessage (errorMessage); | |||
} | |||
else | |||
{ | |||
hideErrorMessage(); | |||
auto& licenseController = ProjucerApplication::getApp().getLicenseController(); | |||
licenseController.setState (newState); | |||
mainWindow.hideLoginFormOverlay(); | |||
ProjucerApplication::getApp().getCommandManager().commandStatusChanged(); | |||
} | |||
})); | |||
auto completionCallback = [this, weakThis] (const String& errorMessage) | |||
{ | |||
if (weakThis == nullptr) | |||
return; | |||
updateLoginButtonStates (false); | |||
if (errorMessage.isNotEmpty()) | |||
{ | |||
showErrorMessage (errorMessage); | |||
} | |||
else | |||
{ | |||
hideErrorMessage(); | |||
mainWindow.hideLoginFormOverlay(); | |||
ProjucerApplication::getApp().getCommandManager().commandStatusChanged(); | |||
} | |||
}; | |||
ProjucerApplication::getApp().getLicenseController().signIn (emailBox.getText(), passwordBox.getText(), | |||
std::move (completionCallback)); | |||
} | |||
String checkLoginFormsAreValid() const | |||
@@ -263,11 +263,9 @@ private: | |||
findColour (treeIconColourId), | |||
findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)), | |||
findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) }; | |||
UserAvatarComponent userAvatar { false, false }; | |||
UserAvatarComponent userAvatar { false }; | |||
Label createAccountLabel { {}, "Create an account" }, | |||
errorMessageLabel { {}, {} }; | |||
std::unique_ptr<LicenseQueryThread> licenseQueryThread; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LoginFormComponent) | |||
}; |
@@ -1087,7 +1087,7 @@ void ProjucerApplication::getCommandInfo (CommandID commandID, ApplicationComman | |||
if (licenseState.isGPL()) | |||
result.setInfo ("Disable GPL mode", "Disables GPL mode", CommandCategories::general, 0); | |||
else | |||
result.setInfo (licenseState.isValid() ? String ("Sign out ") + licenseState.username + "..." : String ("Sign in..."), | |||
result.setInfo (licenseState.isSignedIn() ? String ("Sign out ") + licenseState.username + "..." : String ("Sign in..."), | |||
"Sign out of your JUCE account", | |||
CommandCategories::general, 0); | |||
break; | |||
@@ -1349,7 +1349,11 @@ void ProjucerApplication::launchTutorialsBrowser() | |||
void ProjucerApplication::doLoginOrLogout() | |||
{ | |||
if (licenseController->getCurrentState().type == LicenseState::Type::none) | |||
if (licenseController->getCurrentState().isSignedIn()) | |||
{ | |||
licenseController->resetState(); | |||
} | |||
else | |||
{ | |||
if (auto* window = mainWindowList.getMainWindowWithLoginFormOpen()) | |||
{ | |||
@@ -1361,10 +1365,6 @@ void ProjucerApplication::doLoginOrLogout() | |||
mainWindowList.getFrontmostWindow()->showLoginFormOverlay(); | |||
} | |||
} | |||
else | |||
{ | |||
licenseController->resetState(); | |||
} | |||
} | |||
//============================================================================== | |||
@@ -87,3 +87,6 @@ enum ColourIds | |||
widgetBackgroundColourId = 0x2340010, | |||
secondaryWidgetBackgroundColourId = 0x2340011, | |||
}; | |||
//============================================================================== | |||
static constexpr int projucerMajorVersion = 5; |
@@ -94,7 +94,7 @@ private: | |||
Label configLabel { "Config Label", "Selected exporter" }, projectNameLabel; | |||
ImageComponent juceIcon; | |||
UserAvatarComponent userAvatar { true, true }; | |||
UserAvatarComponent userAvatar { true }; | |||
IconButton projectSettingsButton { "Project Settings", getIcons().settings }, | |||
saveAndOpenInIDEButton { "Save and Open in IDE", Image() }, | |||
@@ -245,12 +245,12 @@ private: | |||
}; | |||
//============================================================================== | |||
void valueTreePropertyChanged (ValueTree&, const Identifier&) override { triggerAsyncUpdate(); } | |||
void valueTreeChildAdded (ValueTree&, ValueTree&) override { triggerAsyncUpdate(); } | |||
void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { triggerAsyncUpdate(); } | |||
void valueTreeChildOrderChanged (ValueTree&, int, int) override { triggerAsyncUpdate(); } | |||
void valueTreeParentChanged (ValueTree&) override { triggerAsyncUpdate(); } | |||
void valueTreeRedirected (ValueTree&) override { triggerAsyncUpdate(); } | |||
void valueTreePropertyChanged (ValueTree&, const Identifier&) override { messagesChanged(); } | |||
void valueTreeChildAdded (ValueTree&, ValueTree&) override { messagesChanged(); } | |||
void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { messagesChanged(); } | |||
void valueTreeChildOrderChanged (ValueTree&, int, int) override { messagesChanged(); } | |||
void valueTreeParentChanged (ValueTree&) override { messagesChanged(); } | |||
void valueTreeRedirected (ValueTree&) override { messagesChanged(); } | |||
void handleAsyncUpdate() override | |||
{ | |||
@@ -20,18 +20,18 @@ | |||
#include "../../Application/jucer_Application.h" | |||
//============================================================================== | |||
class UserAvatarComponent : public Component, | |||
public SettableTooltipClient, | |||
public ChangeBroadcaster, | |||
private LicenseController::LicenseStateListener | |||
{ | |||
public: | |||
UserAvatarComponent (bool tooltip, bool signIn) | |||
: displayTooltip (tooltip), | |||
signInOnClick (signIn) | |||
UserAvatarComponent (bool isInteractive) | |||
: interactive (isInteractive) | |||
{ | |||
ProjucerApplication::getApp().getLicenseController().addListener (this); | |||
licenseStateChanged(); | |||
lookAndFeelChanged(); | |||
} | |||
~UserAvatarComponent() override | |||
@@ -53,12 +53,12 @@ public: | |||
g.reduceClipRegion (ellipse); | |||
} | |||
g.drawImage (userAvatarImage, bounds.toFloat(), RectanglePlacement::fillDestination); | |||
g.drawImage (currentAvatar, bounds.toFloat(), RectanglePlacement::fillDestination); | |||
} | |||
void mouseUp (const MouseEvent&) override | |||
{ | |||
if (signInOnClick) | |||
if (interactive) | |||
{ | |||
PopupMenu menu; | |||
menu.addCommandItem (ProjucerApplication::getApp().commandManager.get(), CommandIDs::loginLogout); | |||
@@ -70,7 +70,25 @@ public: | |||
bool isDisplaingGPLLogo() const noexcept { return isGPL; } | |||
private: | |||
Image createDefaultAvatarImage() | |||
//============================================================================== | |||
static Image createGPLAvatarImage() | |||
{ | |||
if (auto logo = Drawable::createFromImageData (BinaryData::gpl_logo_svg, BinaryData::gpl_logo_svgSize)) | |||
{ | |||
auto bounds = logo->getDrawableBounds(); | |||
Image image (Image::ARGB, roundToInt (bounds.getWidth()), roundToInt (bounds.getHeight()), true); | |||
Graphics g (image); | |||
logo->draw (g, 1.0f); | |||
return image; | |||
} | |||
jassertfalse; | |||
return {}; | |||
} | |||
Image createStandardAvatarImage() | |||
{ | |||
Image image (Image::ARGB, 250, 250, true); | |||
Graphics g (image); | |||
@@ -87,17 +105,18 @@ private: | |||
return image; | |||
} | |||
//============================================================================== | |||
void licenseStateChanged() override | |||
{ | |||
auto state = ProjucerApplication::getApp().getLicenseController().getCurrentState(); | |||
isGPL = ProjucerApplication::getApp().getLicenseController().getCurrentState().isGPL(); | |||
if (displayTooltip) | |||
if (interactive) | |||
{ | |||
auto formattedUserString = [state]() -> String | |||
{ | |||
if (state.isValid()) | |||
if (state.isSignedIn()) | |||
return (state.isGPL() ? "" : (state.username + " - ")) + state.getLicenseTypeString(); | |||
return "Not logged in"; | |||
@@ -106,18 +125,26 @@ private: | |||
setTooltip (formattedUserString); | |||
} | |||
userAvatarImage = state.isValid() && state.avatar.isValid() ? state.avatar : defaultAvatarImage; | |||
currentAvatar = isGPL ? gplAvatarImage | |||
: state.isSignedIn() ? standardAvatarImage : signedOutAvatarImage; | |||
repaint(); | |||
sendChangeMessage(); | |||
} | |||
void lookAndFeelChanged() override | |||
{ | |||
defaultAvatarImage = createDefaultAvatarImage(); | |||
standardAvatarImage = createStandardAvatarImage(); | |||
signedOutAvatarImage = createStandardAvatarImage(); | |||
if (interactive) | |||
signedOutAvatarImage.multiplyAllAlphas (0.4f); | |||
licenseStateChanged(); | |||
repaint(); | |||
} | |||
Image userAvatarImage, defaultAvatarImage { createDefaultAvatarImage() }; | |||
bool isGPL = false, displayTooltip = false, signInOnClick = false; | |||
//============================================================================== | |||
Image standardAvatarImage, signedOutAvatarImage, gplAvatarImage { createGPLAvatarImage() }, currentAvatar; | |||
bool isGPL = false, interactive = false; | |||
}; |
@@ -735,7 +735,7 @@ bool Project::hasIncompatibleLicenseTypeAndSplashScreenSetting() const | |||
|| companyName == "ROLI Ltd."); | |||
return ! ProjucerApplication::getApp().isRunningCommandLine && ! isJUCEProject && ! shouldDisplaySplashScreen() | |||
&& ! ProjucerApplication::getApp().getLicenseController().getCurrentState().isPaidOrGPL(); | |||
&& ! ProjucerApplication::getApp().getLicenseController().getCurrentState().canUnlockFullFeatures(); | |||
} | |||
bool Project::isSaveAndExportDisabled() const | |||
@@ -747,9 +747,15 @@ void Project::updateLicenseWarning() | |||
{ | |||
if (hasIncompatibleLicenseTypeAndSplashScreenSetting()) | |||
{ | |||
ProjectMessages::MessageAction action; | |||
if (ProjucerApplication::getApp().getLicenseController().getCurrentState().isOldLicense()) | |||
action = { "Upgrade", [] { URL ("https://juce.com/get-juce").launchInDefaultBrowser(); } }; | |||
else | |||
action = { "Sign in", [this] { ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (getFile())->showLoginFormOverlay(); } }; | |||
addProjectMessage (ProjectMessages::Ids::incompatibleLicense, | |||
{ { "Sign in", [this] { ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (getFile())->showLoginFormOverlay(); } }, | |||
{ "Enable splash screen", [this] { displaySplashScreenValue = true; } } }); | |||
{ std::move (action), { "Enable splash screen", [this] { displaySplashScreenValue = true; } } }); | |||
} | |||
else | |||
{ | |||