diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index de66be4a88..87b784bc4e 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -36,6 +36,7 @@ Rectangle { property bool pendingInventoryReply: true; property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; + property int pendingItemCount: 0; // Style color: hifi.colors.white; Connections { @@ -79,18 +80,22 @@ Rectangle { onInventoryResult: { purchasesReceived = true; - if (root.pendingInventoryReply) { - inventoryTimer.start(); - } - if (result.status !== 'success') { console.log("Failed to get purchases", result.message); - } else { + } else if (!purchasesContentsList.dragging) { // Don't modify the view if the user's scrolling var inventoryResult = processInventoryResult(result.data.assets); + var currentIndex = purchasesContentsList.currentIndex === -1 ? 0 : purchasesContentsList.currentIndex; purchasesModel.clear(); purchasesModel.append(inventoryResult); + root.pendingItemCount = 0; + for (var i = 0; i < purchasesModel.count; i++) { + if (purchasesModel.get(i).status === "pending") { + root.pendingItemCount++; + } + } + if (previousPurchasesModel.count !== 0) { checkIfAnyItemStatusChanged(); } else { @@ -103,6 +108,12 @@ Rectangle { previousPurchasesModel.append(inventoryResult); buildFilteredPurchasesModel(); + + purchasesContentsList.positionViewAtIndex(currentIndex, ListView.Beginning); + } + + if (root.pendingInventoryReply && root.pendingItemCount > 0) { + inventoryTimer.start(); } root.pendingInventoryReply = false; @@ -419,6 +430,8 @@ Rectangle { visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); clip: true; model: filteredPurchasesModel; + snapMode: ListView.SnapToItem; + highlightRangeMode: ListView.StrictlyEnforceRange; // Anchors anchors.top: root.canRezCertifiedItems ? separator.bottom : cantRezCertified.bottom; anchors.topMargin: 12; diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 42ee44d584..780e08caf8 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -25,8 +25,12 @@ Item { HifiConstants { id: hifi; } id: root; - property bool historyReceived: false; + property bool initialHistoryReceived: false; + property bool historyRequestPending: true; + property bool noMoreHistoryData: false; property int pendingCount: 0; + property int currentHistoryPage: 1; + property var pagesAlreadyAdded: new Array(); Connections { target: Commerce; @@ -36,32 +40,86 @@ Item { } onHistoryResult : { - historyReceived = true; - if (result.status === 'success') { - var sameItemCount = 0; - tempTransactionHistoryModel.clear(); - - tempTransactionHistoryModel.append(result.data.history); - - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - if (!transactionHistoryModel.get(i)) { - sameItemCount = -1; - break; - } else if (tempTransactionHistoryModel.get(i).transaction_type === transactionHistoryModel.get(i).transaction_type && - tempTransactionHistoryModel.get(i).text === transactionHistoryModel.get(i).text) { - sameItemCount++; - } - } + root.initialHistoryReceived = true; + root.historyRequestPending = false; - if (sameItemCount !== tempTransactionHistoryModel.count) { - transactionHistoryModel.clear(); + if (result.status === 'success') { + var currentPage = parseInt(result.current_page); + + if (result.data.history.length === 0) { + root.noMoreHistoryData = true; + console.log("No more data to retrieve from Commerce.history() endpoint.") + } else if (root.currentHistoryPage === 1) { + var sameItemCount = 0; + tempTransactionHistoryModel.clear(); + + tempTransactionHistoryModel.append(result.data.history); + for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - transactionHistoryModel.append(tempTransactionHistoryModel.get(i)); + if (!transactionHistoryModel.get(i)) { + sameItemCount = -1; + break; + } else if (tempTransactionHistoryModel.get(i).transaction_type === transactionHistoryModel.get(i).transaction_type && + tempTransactionHistoryModel.get(i).text === transactionHistoryModel.get(i).text) { + sameItemCount++; + } + } + + if (sameItemCount !== tempTransactionHistoryModel.count) { + transactionHistoryModel.clear(); + for (var i = 0; i < tempTransactionHistoryModel.count; i++) { + transactionHistoryModel.append(tempTransactionHistoryModel.get(i)); + } + calculatePendingAndInvalidated(); + } + } else { + if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) { + console.log("Page " + currentPage + " of history has already been added to the list."); + } else { + // First, add the history result to a temporary model + tempTransactionHistoryModel.clear(); + tempTransactionHistoryModel.append(result.data.history); + + // Make a note that we've already added this page to the model... + root.pagesAlreadyAdded.push(currentPage); + + var insertionIndex = 0; + // If there's nothing in the model right now, we don't need to modify insertionIndex. + if (transactionHistoryModel.count !== 0) { + var currentIteratorPage; + // Search through the whole transactionHistoryModel and look for the insertion point. + // The insertion point is found when the result page from the server is less than + // the page that the current item came from, OR when we've reached the end of the whole model. + for (var i = 0; i < transactionHistoryModel.count; i++) { + currentIteratorPage = transactionHistoryModel.get(i).resultIsFromPage; + + if (currentPage < currentIteratorPage) { + insertionIndex = i; + break; + } else if (i === transactionHistoryModel.count - 1) { + insertionIndex = i + 1; + break; + } + } + } + + // Go through the results we just got back from the server, setting the "resultIsFromPage" + // property of those results and adding them to the main model. + for (var i = 0; i < tempTransactionHistoryModel.count; i++) { + tempTransactionHistoryModel.setProperty(i, "resultIsFromPage", currentPage); + transactionHistoryModel.insert(i + insertionIndex, tempTransactionHistoryModel.get(i)) + } + + calculatePendingAndInvalidated(); } - calculatePendingAndInvalidated(); } } - refreshTimer.start(); + + // Only auto-refresh if the user hasn't scrolled + // and there is more data to grab + if (transactionHistory.atYBeginning && !root.noMoreHistoryData) { + refreshTimer.start(); + } } } @@ -134,9 +192,13 @@ Item { onVisibleChanged: { if (visible) { - historyReceived = false; + transactionHistoryModel.clear(); Commerce.balance(); - Commerce.history(); + initialHistoryReceived = false; + root.currentHistoryPage = 1; + root.noMoreHistoryData = false; + root.historyRequestPending = true; + Commerce.history(root.currentHistoryPage); } else { refreshTimer.stop(); } @@ -164,9 +226,12 @@ Item { id: refreshTimer; interval: 4000; onTriggered: { - console.log("Refreshing Wallet Home..."); - Commerce.balance(); - Commerce.history(); + if (transactionHistory.atYBeginning) { + console.log("Refreshing 1st Page of Recent Activity..."); + root.historyRequestPending = true; + Commerce.balance(); + Commerce.history(1); + } } } @@ -241,7 +306,7 @@ Item { anchors.right: parent.right; Item { - visible: transactionHistoryModel.count === 0 && root.historyReceived; + visible: transactionHistoryModel.count === 0 && root.initialHistoryReceived; anchors.centerIn: parent; width: parent.width - 12; height: parent.height; @@ -364,7 +429,12 @@ Item { onAtYEndChanged: { if (transactionHistory.atYEnd) { console.log("User scrolled to the bottom of 'Recent Activity'."); - // Grab next page of results and append to model + if (!root.historyRequestPending && !root.noMoreHistoryData) { + // Grab next page of results and append to model + root.historyRequestPending = true; + Commerce.history(++root.currentHistoryPage); + console.log("Fetching Page " + root.currentHistoryPage + " of Recent Activity..."); + } } } } diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index d643574810..21d025fd30 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -244,6 +244,7 @@ Item { PageIndicator { id: pageIndicator currentIndex: swipeView.currentIndex + visible: swipeView.count > 1 delegate: Item { width: 15 diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index d7d36dabf6..51658ddef8 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -72,11 +72,16 @@ void Ledger::signedSend(const QString& propertyName, const QByteArray& text, con send(endpoint, success, fail, QNetworkAccessManager::PutOperation, AccountManagerAuth::Required, request); } -void Ledger::keysQuery(const QString& endpoint, const QString& success, const QString& fail) { +void Ledger::keysQuery(const QString& endpoint, const QString& success, const QString& fail, QJsonObject& requestParams) { auto wallet = DependencyManager::get(); - QJsonObject request; - request["public_keys"] = QJsonArray::fromStringList(wallet->listPublicKeys()); - send(endpoint, success, fail, QNetworkAccessManager::PostOperation, AccountManagerAuth::Required, request); + requestParams["public_keys"] = QJsonArray::fromStringList(wallet->listPublicKeys()); + + send(endpoint, success, fail, QNetworkAccessManager::PostOperation, AccountManagerAuth::Required, requestParams); +} + +void Ledger::keysQuery(const QString& endpoint, const QString& success, const QString& fail) { + QJsonObject requestParams; + keysQuery(endpoint, success, fail, requestParams); } void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure) { @@ -169,6 +174,7 @@ void Ledger::historySuccess(QNetworkReply& reply) { QJsonObject newDataData; newDataData["history"] = newHistoryArray; newData["data"] = newDataData; + newData["current_page"] = data["current_page"].toInt(); emit historyResult(newData); } @@ -176,8 +182,11 @@ void Ledger::historyFailure(QNetworkReply& reply) { failResponse("history", reply); } -void Ledger::history(const QStringList& keys) { - keysQuery("history", "historySuccess", "historyFailure"); +void Ledger::history(const QStringList& keys, const int& pageNumber) { + QJsonObject params; + params["per_page"] = 100; + params["page"] = pageNumber; + keysQuery("history", "historySuccess", "historyFailure", params); } // The api/failResponse is called just for the side effect of logging. diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 42eb0ffc49..5d90aa0808 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -29,7 +29,7 @@ public: bool receiveAt(const QString& hfc_key, const QString& old_key); void balance(const QStringList& keys); void inventory(const QStringList& keys); - void history(const QStringList& keys); + void history(const QStringList& keys, const int& pageNumber); void account(); void reset(); void updateLocation(const QString& asset_id, const QString location, const bool controlledFailure = false); @@ -79,6 +79,7 @@ private: QJsonObject apiResponse(const QString& label, QNetworkReply& reply); QJsonObject failResponse(const QString& label, QNetworkReply& reply); void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request); + void keysQuery(const QString& endpoint, const QString& success, const QString& fail, QJsonObject& extraRequestParams); void keysQuery(const QString& endpoint, const QString& success, const QString& fail); void signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail, const bool controlled_failure = false); }; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 62e87f9c66..320c7e041c 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -96,12 +96,12 @@ void QmlCommerce::inventory() { } } -void QmlCommerce::history() { +void QmlCommerce::history(const int& pageNumber) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->history(cachedPublicKeys); + ledger->history(cachedPublicKeys, pageNumber); } } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index c53e73d565..f2e6c82021 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -60,7 +60,7 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); Q_INVOKABLE void inventory(); - Q_INVOKABLE void history(); + Q_INVOKABLE void history(const int& pageNumber); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void reset(); Q_INVOKABLE void resetLocalWalletOnly(); diff --git a/libraries/midi/src/Midi.cpp b/libraries/midi/src/Midi.cpp index 6c2471f680..69c35c4a20 100644 --- a/libraries/midi/src/Midi.cpp +++ b/libraries/midi/src/Midi.cpp @@ -3,6 +3,7 @@ // libraries/midi/src // // Created by Burt Sloane +// Modified by Bruce Brown // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -14,30 +15,45 @@ #include - #if defined Q_OS_WIN32 #include "Windows.h" #endif - #if defined Q_OS_WIN32 const int MIDI_BYTE_MASK = 0x0FF; +const int MIDI_NIBBLE_MASK = 0x00F; +const int MIDI_PITCH_BEND_MASK = 0x3F80; +const int MIDI_SHIFT_STATUS = 4; const int MIDI_SHIFT_NOTE = 8; const int MIDI_SHIFT_VELOCITY = 16; +const int MIDI_SHIFT_PITCH_BEND = 9; +// Status Decode +const int MIDI_NOTE_OFF = 0x8; +const int MIDI_NOTE_ON = 0x9; +const int MIDI_POLYPHONIC_KEY_PRESSURE = 0xa; +const int MIDI_PROGRAM_CHANGE = 0xc; +const int MIDI_CHANNEL_PRESSURE = 0xd; +const int MIDI_PITCH_BEND_CHANGE = 0xe; +const int MIDI_SYSTEM_MESSAGE = 0xf; #endif -const int MIDI_STATUS_MASK = 0x0F0; -const int MIDI_NOTE_OFF = 0x080; -const int MIDI_NOTE_ON = 0x090; -const int MIDI_CONTROL_CHANGE = 0x0b0; + +const int MIDI_CONTROL_CHANGE = 0xb; const int MIDI_CHANNEL_MODE_ALL_NOTES_OFF = 0x07b; - -static Midi* instance = NULL; // communicate this to non-class callbacks +static Midi* instance = NULL; // communicate this to non-class callbacks static bool thruModeEnabled = false; +static bool broadcastEnabled = false; +static bool typeNoteOffEnabled = true; +static bool typeNoteOnEnabled = true; +static bool typePolyKeyPressureEnabled = false; +static bool typeControlChangeEnabled = true; +static bool typeProgramChangeEnabled = true; +static bool typeChanPressureEnabled = false; +static bool typePitchBendEnabled = true; +static bool typeSystemMessageEnabled = false; -std::vector Midi::midiinexclude; -std::vector Midi::midioutexclude; - +std::vector Midi::midiInExclude; +std::vector Midi::midiOutExclude; #if defined Q_OS_WIN32 @@ -47,7 +63,6 @@ std::vector Midi::midioutexclude; std::vector midihin; std::vector midihout; - void CALLBACK MidiInProc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { switch (wMsg) { case MIM_OPEN: @@ -58,23 +73,64 @@ void CALLBACK MidiInProc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD if (midihin[i] == hMidiIn) { midihin[i] = NULL; instance->allNotesOff(); + instance->midiHardwareChange(); } } break; case MIM_DATA: { - int status = MIDI_BYTE_MASK & dwParam1; - int note = MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_NOTE); - int vel = MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_VELOCITY); - if (thruModeEnabled) { - instance->sendNote(status, note, vel); // relay the note on to all other midi devices + int device = -1; + for (int i = 0; i < midihin.size(); i++) { + if (midihin[i] == hMidiIn) { + device = i; + } } - instance->noteReceived(status, note, vel); // notify the javascript + int raw = dwParam1; + int channel = (MIDI_NIBBLE_MASK & dwParam1) + 1; + int status = MIDI_BYTE_MASK & dwParam1; + int type = MIDI_NIBBLE_MASK & (dwParam1 >> MIDI_SHIFT_STATUS); + int note = MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_NOTE); + int velocity = MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_VELOCITY); + int bend = 0; + int program = 0; + if (!typeNoteOffEnabled && type == MIDI_NOTE_OFF) { + return; + } + if (!typeNoteOnEnabled && type == MIDI_NOTE_ON) { + return; + } + if (!typePolyKeyPressureEnabled && type == MIDI_POLYPHONIC_KEY_PRESSURE) { + return; + } + if (!typeControlChangeEnabled && type == MIDI_CONTROL_CHANGE) { + return; + } + if (typeProgramChangeEnabled && type == MIDI_PROGRAM_CHANGE) { + program = note; + note = 0; + } + if (typeChanPressureEnabled && type == MIDI_CHANNEL_PRESSURE) { + velocity = note; + note = 0; + } + if (typePitchBendEnabled && type == MIDI_PITCH_BEND_CHANGE) { + bend = ((MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_NOTE)) | + (MIDI_PITCH_BEND_MASK & (dwParam1 >> MIDI_SHIFT_PITCH_BEND))) - 8192; + channel = 0; // Weird values on different instruments + note = 0; + velocity = 0; + } + if (!typeSystemMessageEnabled && type == MIDI_SYSTEM_MESSAGE) { + return; + } + if (thruModeEnabled) { + instance->sendNote(status, note, velocity); // relay the message on to all other midi devices. + } + instance->midiReceived(device, raw, channel, status, type, note, velocity, bend, program); // notify the javascript break; } } } - void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { switch (wMsg) { case MOM_OPEN: @@ -85,21 +141,45 @@ void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_P if (midihout[i] == hmo) { midihout[i] = NULL; instance->allNotesOff(); + instance->midiHardwareChange(); } } break; } } - -void Midi::sendNote(int status, int note, int vel) { - for (int i = 0; i < midihout.size(); i++) { - if (midihout[i] != NULL) { - midiOutShortMsg(midihout[i], status + (note << MIDI_SHIFT_NOTE) + (vel << MIDI_SHIFT_VELOCITY)); +void Midi::sendRawMessage(int device, int raw) { + if (broadcastEnabled) { + for (int i = 0; i < midihout.size(); i++) { + if (midihout[i] != NULL) { + midiOutShortMsg(midihout[i], raw); + } } + } else { + midiOutShortMsg(midihout[device], raw); } } +void Midi::sendMessage(int device, int channel, int type, int note, int velocity) { + int message = (channel - 1) | (type << MIDI_SHIFT_STATUS); + if (broadcastEnabled) { + for (int i = 0; i < midihout.size(); i++) { + if (midihout[i] != NULL) { + midiOutShortMsg(midihout[i], message | (note << MIDI_SHIFT_NOTE) | (velocity << MIDI_SHIFT_VELOCITY)); + } + } + } else { + midiOutShortMsg(midihout[device], message | (note << MIDI_SHIFT_NOTE) | (velocity << MIDI_SHIFT_VELOCITY)); + } +} + +void Midi::sendNote(int status, int note, int velocity) { + for (int i = 0; i < midihout.size(); i++) { + if (midihout[i] != NULL) { + midiOutShortMsg(midihout[i], status + (note << MIDI_SHIFT_NOTE) + (velocity << MIDI_SHIFT_VELOCITY)); + } + } +} void Midi::MidiSetup() { midihin.clear(); @@ -110,8 +190,8 @@ void Midi::MidiSetup() { midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS)); bool found = false; - for (int j = 0; j < midiinexclude.size(); j++) { - if (midiinexclude[j].toStdString().compare(incaps.szPname) == 0) { + for (int j = 0; j < midiInExclude.size(); j++) { + if (midiInExclude[j].toStdString().compare(incaps.szPname) == 0) { found = true; break; } @@ -122,7 +202,6 @@ void Midi::MidiSetup() { midiInStart(tmphin); midihin.push_back(tmphin); } - } MIDIOUTCAPS outcaps; @@ -130,8 +209,8 @@ void Midi::MidiSetup() { midiOutGetDevCaps(i, &outcaps, sizeof(MIDIINCAPS)); bool found = false; - for (int j = 0; j < midioutexclude.size(); j++) { - if (midioutexclude[j].toStdString().compare(outcaps.szPname) == 0) { + for (int j = 0; j < midiOutExclude.size(); j++) { + if (midiOutExclude[j].toStdString().compare(outcaps.szPname) == 0) { found = true; break; } @@ -164,7 +243,13 @@ void Midi::MidiCleanup() { midihout.clear(); } #else -void Midi::sendNote(int status, int note, int vel) { +void Midi::sendRawMessage(int device, int raw) { +} + +void Midi::sendNote(int status, int note, int velocity) { +} + +void Midi::sendMessage(int device, int channel, int type, int note, int velocity){ } void Midi::MidiSetup() { @@ -176,26 +261,30 @@ void Midi::MidiCleanup() { } #endif -void Midi::noteReceived(int status, int note, int velocity) { - if (((status & MIDI_STATUS_MASK) != MIDI_NOTE_OFF) && - ((status & MIDI_STATUS_MASK) != MIDI_NOTE_ON) && - ((status & MIDI_STATUS_MASK) != MIDI_CONTROL_CHANGE)) { - return; // NOTE: only sending note-on, note-off, and control-change to Javascript - } - +void Midi::midiReceived(int device, int raw, int channel, int status, int type, int note, int velocity, int bend, int program) { QVariantMap eventData; + eventData["device"] = device; + eventData["raw"] = raw; + eventData["channel"] = channel; eventData["status"] = status; + eventData["type"] = type; eventData["note"] = note; eventData["velocity"] = velocity; - emit midiNote(eventData); + eventData["bend"] = bend; + eventData["program"] = program; + emit midiNote(eventData);// Legacy + emit midiMessage(eventData); } +void Midi::midiHardwareChange() { + emit midiReset(); +} // Midi::Midi() { instance = this; #if defined Q_OS_WIN32 - midioutexclude.push_back("Microsoft GS Wavetable Synth"); // we don't want to hear this thing + midiOutExclude.push_back("Microsoft GS Wavetable Synth"); // we don't want to hear this thing (Lags) #endif MidiSetup(); } @@ -203,10 +292,18 @@ Midi::Midi() { Midi::~Midi() { } +void Midi::sendRawDword(int device, int raw) { + sendRawMessage(device, raw); +} + void Midi::playMidiNote(int status, int note, int velocity) { sendNote(status, note, velocity); } +void Midi::sendMidiMessage(int device, int channel, int type, int note, int velocity) { + sendMessage(device, channel, type, note, velocity); +} + void Midi::allNotesOff() { sendNote(MIDI_CONTROL_CHANGE, MIDI_CHANNEL_MODE_ALL_NOTES_OFF, 0); // all notes off } @@ -219,6 +316,7 @@ void Midi::resetDevices() { void Midi::USBchanged() { instance->MidiCleanup(); instance->MidiSetup(); + instance->midiHardwareChange(); } // @@ -245,16 +343,16 @@ QStringList Midi::listMidiDevices(bool output) { void Midi::unblockMidiDevice(QString name, bool output) { if (output) { - for (unsigned long i = 0; i < midioutexclude.size(); i++) { - if (midioutexclude[i].toStdString().compare(name.toStdString()) == 0) { - midioutexclude.erase(midioutexclude.begin() + i); + for (unsigned long i = 0; i < midiOutExclude.size(); i++) { + if (midiOutExclude[i].toStdString().compare(name.toStdString()) == 0) { + midiOutExclude.erase(midiOutExclude.begin() + i); break; } } } else { - for (unsigned long i = 0; i < midiinexclude.size(); i++) { - if (midiinexclude[i].toStdString().compare(name.toStdString()) == 0) { - midiinexclude.erase(midiinexclude.begin() + i); + for (unsigned long i = 0; i < midiInExclude.size(); i++) { + if (midiInExclude[i].toStdString().compare(name.toStdString()) == 0) { + midiInExclude.erase(midiInExclude.begin() + i); break; } } @@ -264,9 +362,9 @@ void Midi::unblockMidiDevice(QString name, bool output) { void Midi::blockMidiDevice(QString name, bool output) { unblockMidiDevice(name, output); // make sure it's only in there once if (output) { - midioutexclude.push_back(name); + midiOutExclude.push_back(name); } else { - midiinexclude.push_back(name); + midiInExclude.push_back(name); } } @@ -274,3 +372,38 @@ void Midi::thruModeEnable(bool enable) { thruModeEnabled = enable; } +void Midi::broadcastEnable(bool enable) { + broadcastEnabled = enable; +} + +void Midi::typeNoteOffEnable(bool enable) { + typeNoteOffEnabled = enable; +} + +void Midi::typeNoteOnEnable(bool enable) { + typeNoteOnEnabled = enable; +} + +void Midi::typePolyKeyPressureEnable(bool enable) { + typePolyKeyPressureEnabled = enable; +} + +void Midi::typeControlChangeEnable(bool enable) { + typeControlChangeEnabled = enable; +} + +void Midi::typeProgramChangeEnable(bool enable) { + typeProgramChangeEnabled = enable; +} + +void Midi::typeChanPressureEnable(bool enable) { + typeChanPressureEnabled = enable; +} + +void Midi::typePitchBendEnable(bool enable) { + typePitchBendEnabled = enable; +} + +void Midi::typeSystemMessageEnable(bool enable) { + typeSystemMessageEnabled = enable; +} diff --git a/libraries/midi/src/Midi.h b/libraries/midi/src/Midi.h index 013ec056e3..f7940bbe5d 100644 --- a/libraries/midi/src/Midi.h +++ b/libraries/midi/src/Midi.h @@ -3,6 +3,7 @@ // libraries/midi/src // // Created by Burt Sloane +// Modified by Bruce Brown // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -24,13 +25,16 @@ class Midi : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - void noteReceived(int status, int note, int velocity); // relay a note to Javascript - void sendNote(int status, int note, int vel); // relay a note to MIDI outputs + void midiReceived(int device, int raw, int channel, int status, int type, int note, int velocity, int bend, int program); // relay a note to Javascript + void midiHardwareChange(); // relay hardware change to Javascript + void sendRawMessage(int device, int raw); // relay midi message to MIDI outputs + void sendNote(int status, int note, int velocity); // relay a note to MIDI outputs + void sendMessage(int device, int channel, int type, int note, int velocity); // relay a message to MIDI outputs static void USBchanged(); private: - static std::vector midiinexclude; - static std::vector midioutexclude; + static std::vector midiInExclude; + static std::vector midiOutExclude; private: void MidiSetup(); @@ -38,31 +42,63 @@ private: signals: void midiNote(QVariantMap eventData); + void midiMessage(QVariantMap eventData); + void midiReset(); -public slots: -/// play a note on all connected devices -/// @param {int} status: 0x80 is noteoff, 0x90 is noteon (if velocity=0, noteoff), etc -/// @param {int} note: midi note number -/// @param {int} velocity: note velocity (0 means noteoff) -Q_INVOKABLE void playMidiNote(int status, int note, int velocity); + public slots: + // Send Raw Midi Packet to all connected devices + Q_INVOKABLE void sendRawDword(int device, int raw); + /// Send Raw Midi message to selected device + /// @param {int} device: device number + /// @param {int} raw: raw midi message (DWORD) -/// turn off all notes on all connected devices -Q_INVOKABLE void allNotesOff(); + // Send Midi Message to all connected devices + Q_INVOKABLE void sendMidiMessage(int device, int channel, int type, int note, int velocity); + /// Send midi message to selected device/devices + /// @param {int} device: device number + /// @param {int} channel: channel number + /// @param {int} type: 0x8 is noteoff, 0x9 is noteon (if velocity=0, noteoff), etc + /// @param {int} note: midi note number + /// @param {int} velocity: note velocity (0 means noteoff) -/// clean up and re-discover attached devices -Q_INVOKABLE void resetDevices(); + // Send Midi Message to all connected devices + Q_INVOKABLE void playMidiNote(int status, int note, int velocity); + /// play a note on all connected devices + /// @param {int} status: 0x80 is noteoff, 0x90 is noteon (if velocity=0, noteoff), etc + /// @param {int} note: midi note number + /// @param {int} velocity: note velocity (0 means noteoff) -/// ask for a list of inputs/outputs -Q_INVOKABLE QStringList listMidiDevices(bool output); + /// turn off all notes on all connected devices + Q_INVOKABLE void allNotesOff(); -/// block an input/output by name -Q_INVOKABLE void blockMidiDevice(QString name, bool output); + /// clean up and re-discover attached devices + Q_INVOKABLE void resetDevices(); -/// unblock an input/output by name -Q_INVOKABLE void unblockMidiDevice(QString name, bool output); + /// ask for a list of inputs/outputs + Q_INVOKABLE QStringList listMidiDevices(bool output); + + /// block an input/output by name + Q_INVOKABLE void blockMidiDevice(QString name, bool output); + + /// unblock an input/output by name + Q_INVOKABLE void unblockMidiDevice(QString name, bool output); + + /// repeat all incoming notes to all outputs (default disabled) + Q_INVOKABLE void thruModeEnable(bool enable); + + /// broadcast on all unblocked devices + Q_INVOKABLE void broadcastEnable(bool enable); + + /// filter by event types + Q_INVOKABLE void typeNoteOffEnable(bool enable); + Q_INVOKABLE void typeNoteOnEnable(bool enable); + Q_INVOKABLE void typePolyKeyPressureEnable(bool enable); + Q_INVOKABLE void typeControlChangeEnable(bool enable); + Q_INVOKABLE void typeProgramChangeEnable(bool enable); + Q_INVOKABLE void typeChanPressureEnable(bool enable); + Q_INVOKABLE void typePitchBendEnable(bool enable); + Q_INVOKABLE void typeSystemMessageEnable(bool enable); -/// repeat all incoming notes to all outputs (default disabled) -Q_INVOKABLE void thruModeEnable(bool enable); public: Midi(); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 7ed20180b8..471e693a0c 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -22,24 +22,25 @@ function pushAll(dest, orig) { if (!App.isAndroid()) { pushAll(DEFAULT_SCRIPTS_COMBINED, [ - "system/progress.js", - "system/away.js", - "system/audio.js", - "system/hmd.js", - "system/menu.js", - "system/bubble.js", - "system/snapshot.js", - "system/help.js", - "system/pal.js", // "system/mod.js", // older UX, if you prefer - "system/makeUserConnection.js", - "system/tablet-goto.js", - "system/marketplaces/marketplaces.js", - "system/commerce/wallet.js", - "system/edit.js", - "system/notifications.js", - "system/dialTone.js", - "system/firstPersonHMD.js", - "system/tablet-ui/tabletUI.js" + "system/progress.js", + "system/away.js", + "system/audio.js", + "system/hmd.js", + "system/menu.js", + "system/bubble.js", + "system/snapshot.js", + "system/help.js", + "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/makeUserConnection.js", + "system/tablet-goto.js", + "system/marketplaces/marketplaces.js", + "system/commerce/wallet.js", + "system/edit.js", + "system/notifications.js", + "system/dialTone.js", + "system/firstPersonHMD.js", + "system/tablet-ui/tabletUI.js", + "system/emote.js" ]); } else { pushAll(DEFAULT_SCRIPTS_COMBINED, [ diff --git a/scripts/system/assets/animations/Cheering.fbx b/scripts/system/assets/animations/Cheering.fbx new file mode 100644 index 0000000000..8787bf4bd8 Binary files /dev/null and b/scripts/system/assets/animations/Cheering.fbx differ diff --git a/scripts/system/assets/animations/Clapping.fbx b/scripts/system/assets/animations/Clapping.fbx new file mode 100644 index 0000000000..d05b41866d Binary files /dev/null and b/scripts/system/assets/animations/Clapping.fbx differ diff --git a/scripts/system/assets/animations/Crying.fbx b/scripts/system/assets/animations/Crying.fbx new file mode 100644 index 0000000000..2e60ba2450 Binary files /dev/null and b/scripts/system/assets/animations/Crying.fbx differ diff --git a/scripts/system/assets/animations/Dancing.fbx b/scripts/system/assets/animations/Dancing.fbx new file mode 100644 index 0000000000..7759d273b7 Binary files /dev/null and b/scripts/system/assets/animations/Dancing.fbx differ diff --git a/scripts/system/assets/animations/Fall.fbx b/scripts/system/assets/animations/Fall.fbx new file mode 100644 index 0000000000..627e909bb4 Binary files /dev/null and b/scripts/system/assets/animations/Fall.fbx differ diff --git a/scripts/system/assets/animations/Pointing.fbx b/scripts/system/assets/animations/Pointing.fbx new file mode 100644 index 0000000000..da3c9bbeca Binary files /dev/null and b/scripts/system/assets/animations/Pointing.fbx differ diff --git a/scripts/system/assets/animations/Surprised.fbx b/scripts/system/assets/animations/Surprised.fbx new file mode 100644 index 0000000000..49362605b3 Binary files /dev/null and b/scripts/system/assets/animations/Surprised.fbx differ diff --git a/scripts/system/assets/animations/Waving.fbx b/scripts/system/assets/animations/Waving.fbx new file mode 100644 index 0000000000..e2442f64f4 Binary files /dev/null and b/scripts/system/assets/animations/Waving.fbx differ diff --git a/scripts/system/assets/sounds/rezzing.wav b/scripts/system/assets/sounds/rezzing.wav new file mode 100644 index 0000000000..3c059aecdf Binary files /dev/null and b/scripts/system/assets/sounds/rezzing.wav differ diff --git a/scripts/system/emote.js b/scripts/system/emote.js new file mode 100644 index 0000000000..f1f739c126 --- /dev/null +++ b/scripts/system/emote.js @@ -0,0 +1,122 @@ +"use strict"; + +// +// emote.js +// scripts/system/ +// +// Created by Brad Hefta-Gaub on 7 Jan 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* globals Script, Tablet */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function() { // BEGIN LOCAL_SCOPE + + +var EMOTE_ANIMATIONS = ['Crying', 'Surprised', 'Dancing', 'Cheering', 'Waving', 'Fall', 'Pointing', 'Clapping']; +var ANIMATIONS = Array(); + + +EMOTE_ANIMATIONS.forEach(function (name) { + var animationURL = Script.resolvePath("assets/animations/" + name + ".fbx"); + var resource = AnimationCache.prefetch(animationURL); + var animation = AnimationCache.getAnimation(animationURL); + ANIMATIONS[name] = { url: animationURL, animation: animation, resource: resource}; +}); + + +var EMOTE_APP_BASE = "html/EmoteApp.html"; +var EMOTE_APP_URL = Script.resolvePath(EMOTE_APP_BASE); +var EMOTE_LABEL = "EMOTE"; +var EMOTE_APP_SORT_ORDER = 11; +var FPS = 60; +var MSEC_PER_SEC = 1000; +var FINISHED = 3; // see ScriptableResource::State + +var onEmoteScreen = false; +var button; +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var activeTimer = false; // used to cancel active timer if a user plays an amimation while another animation is playing +var activeEmote = false; // to keep track of the currently playing emote + +button = tablet.addButton({ + //icon: "icons/tablet-icons/emote.svg", // TODO - we need graphics for this + text: EMOTE_LABEL, + sortOrder: EMOTE_APP_SORT_ORDER +}); + +function onClicked() { + if (onEmoteScreen) { + tablet.gotoHomeScreen(); + } else { + onEmoteScreen = true; + tablet.gotoWebScreen(EMOTE_APP_URL); + } +} + +function onScreenChanged(type, url) { + onEmoteScreen = type === "Web" && (url.indexOf(EMOTE_APP_BASE) == url.length - EMOTE_APP_BASE.length); + button.editProperties({ isActive: onEmoteScreen }); +} + +// Handle the events we're receiving from the web UI +function onWebEventReceived(event) { + + // Converts the event to a JavasScript Object + if (typeof event === "string") { + event = JSON.parse(event); + } + + if (event.type === "click") { + var emoteName = event.data; + + if (ANIMATIONS[emoteName].resource.state == FINISHED) { + if (activeTimer !== false) { + Script.clearTimeout(activeTimer); + } + + // if the activeEmote is different from the chosen emote, then play the new emote. Other wise, + // this is a second click on the same emote as the activeEmote, and we will just stop it. + if (activeEmote !== emoteName) { + activeEmote = emoteName; + var frameCount = ANIMATIONS[emoteName].animation.frames.length; + MyAvatar.overrideAnimation(ANIMATIONS[emoteName].url, FPS, false, 0, frameCount); + + var timeOut = MSEC_PER_SEC * frameCount / FPS; + activeTimer = Script.setTimeout(function () { + MyAvatar.restoreAnimation(); + activeTimer = false; + activeEmote = false; + }, timeOut); + } else { + activeEmote = false; + MyAvatar.restoreAnimation(); + } + } + } +} + +button.clicked.connect(onClicked); +tablet.screenChanged.connect(onScreenChanged); +tablet.webEventReceived.connect(onWebEventReceived); + +Script.scriptEnding.connect(function () { + if (onEmoteScreen) { + tablet.gotoHomeScreen(); + } + button.clicked.disconnect(onClicked); + tablet.screenChanged.disconnect(onScreenChanged); + if (tablet) { + tablet.removeButton(button); + } + if (activeTimer !== false) { + Script.clearTimeout(activeTimer); + MyAvatar.restoreAnimation(); + } +}); + + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/html/EmoteApp.html b/scripts/system/html/EmoteApp.html new file mode 100644 index 0000000000..30ef3e17a1 --- /dev/null +++ b/scripts/system/html/EmoteApp.html @@ -0,0 +1,136 @@ + + + + Emote App + + + + + + +
+

Emote App

+
+
+

Click an emotion to Emote:

+

+

+

+

+

+

+

+

+
+ + + + + \ No newline at end of file diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index a5360974f6..8819960354 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -28,6 +28,7 @@ var selectionDisplay = null; // for gridTool.js to ignore var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; + var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; @@ -341,6 +342,15 @@ var selectionDisplay = null; // for gridTool.js to ignore // we currently assume a wearable is a single entity Entities.editEntity(pastedEntityIDs[0], offsets); } + + var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position; + + Audio.playSound(REZZING_SOUND, { + volume: 1.0, + position: rezPosition, + localOnly: true + }); + } else { Window.notifyEditError("Can't import entities: entities would be out of bounds."); }