diff --git a/LightHost.jucer b/LightHost.jucer index 09ad20b..83acad4 100644 --- a/LightHost.jucer +++ b/LightHost.jucer @@ -1,6 +1,6 @@ - getApplicationName()); }; IconMenu::~IconMenu() { - + savePluginStates(); +} + +void IconMenu::setIcon() +{ + // Set menu icon + #if JUCE_MAC + if (exec("defaults read -g AppleInterfaceStyle").compare("Dark") == 1) + setIconImage(ImageFileFormat::loadFrom(BinaryData::menu_icon_white_png, BinaryData::menu_icon_white_pngSize)); + else + setIconImage(ImageFileFormat::loadFrom(BinaryData::menu_icon_png, BinaryData::menu_icon_pngSize)); + #else + String defaultColor; + #if JUCE_WINDOWS + defaultColor = "white"; + #elif JUCE_LINUX + defaultColor = "black"; + #endif + if (!getAppProperties().getUserSettings()->containsKey("icon")) + getAppProperties().getUserSettings()->setValue("icon", defaultColor); + String color = getAppProperties().getUserSettings()->getValue("icon"); + Image icon; + if (color.equalsIgnoreCase("white")) + icon = ImageFileFormat::loadFrom(BinaryData::menu_icon_white_png, BinaryData::menu_icon_white_pngSize); + else if (color.equalsIgnoreCase("black")) + icon = ImageFileFormat::loadFrom(BinaryData::menu_icon_png, BinaryData::menu_icon_pngSize); + setIconImage(icon); + #endif } void IconMenu::loadActivePlugins() { + const int INPUT = 1000000; + const int OUTPUT = INPUT + 1; + const int CHANNEL_ONE = 0; + const int CHANNEL_TWO = 1; + PluginWindow::closeAllCurrentlyOpenWindows(); graph.clear(); - inputNode = graph.addNode(new AudioProcessorGraph::AudioGraphIOProcessor(AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode), 1); - outputNode = graph.addNode(new AudioProcessorGraph::AudioGraphIOProcessor(AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode), 2); + inputNode = graph.addNode(new AudioProcessorGraph::AudioGraphIOProcessor(AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode), INPUT); + outputNode = graph.addNode(new AudioProcessorGraph::AudioGraphIOProcessor(AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode), OUTPUT); if (activePluginList.getNumTypes() == 0) { - graph.addConnection(1, 0, 2, 0); - graph.addConnection(1, 1, 2, 1); + graph.addConnection(INPUT, CHANNEL_ONE, OUTPUT, CHANNEL_ONE); + graph.addConnection(INPUT, CHANNEL_TWO, OUTPUT, CHANNEL_TWO); } int pluginTime = 0; - for (int i = 0; i < activePluginList.getNumTypes(); i++) + int lastId = 0; + bool hasInputConnected = false; + // NOTE: Node ids cannot begin at 0. + for (int i = 1; i <= activePluginList.getNumTypes(); i++) { PluginDescription plugin = getNextPluginOlderThanTime(pluginTime); String errorMessage; AudioPluginInstance* instance = formatManager.createPluginInstance(plugin, graph.getSampleRate(), graph.getBlockSize(), errorMessage); - String pluginUid; - pluginUid << "pluginState-" << i; + String pluginUid = getKey("state", plugin); String savedPluginState = getAppProperties().getUserSettings()->getValue(pluginUid); MemoryBlock savedPluginBinary; savedPluginBinary.fromBase64Encoding(savedPluginState); instance->setStateInformation(savedPluginBinary.getData(), savedPluginBinary.getSize()); - graph.addNode(instance, i+3); + graph.addNode(instance, i); + String key = getKey("bypass", plugin); + bool bypass = getAppProperties().getUserSettings()->getBoolValue(key, false); // Input to plugin - if (i == 0) + if ((!hasInputConnected) && (!bypass)) { - graph.addConnection(1, 0, i+3, 0); - graph.addConnection(1, 1, i+3, 1); - } - // Plugin to output - if (i == activePluginList.getNumTypes() - 1) - { - graph.addConnection(i+3, 0, 2, 0); - graph.addConnection(i+3, 1, 2, 1); + graph.addConnection(INPUT, CHANNEL_ONE, i, CHANNEL_ONE); + graph.addConnection(INPUT, CHANNEL_TWO, i, CHANNEL_TWO); + hasInputConnected = true; } // Connect previous plugin to current - if (i > 0) + else if (!bypass) { - graph.addConnection(i+2, 0, i+3, 0); - graph.addConnection(i+2, 1, i+3, 1); + graph.addConnection(lastId, CHANNEL_ONE, i, CHANNEL_ONE); + graph.addConnection(lastId, CHANNEL_TWO, i, CHANNEL_TWO); } + if (!bypass) + lastId = i; } + if (lastId > 0) + { + // Last active plugin to output + graph.addConnection(lastId, CHANNEL_ONE, OUTPUT, CHANNEL_ONE); + graph.addConnection(lastId, CHANNEL_TWO, OUTPUT, CHANNEL_TWO); + } } PluginDescription IconMenu::getNextPluginOlderThanTime(int &time) @@ -152,7 +183,7 @@ PluginDescription IconMenu::getNextPluginOlderThanTime(int &time) for (int i = 0; i < activePluginList.getNumTypes(); i++) { PluginDescription plugin = *activePluginList.getType(i); - String key = "pluginOrder-" + plugin.descriptiveName + plugin.version + plugin.pluginFormatName; + String key = getKey("order", plugin); String pluginTimeString = getAppProperties().getUserSettings()->getValue(key); int pluginTime = atoi(pluginTimeString.toStdString().c_str()); if (pluginTime > timeStatic && abs(timeStatic - pluginTime) < diff) @@ -212,24 +243,38 @@ void IconMenu::timerCallback() menu.addItem(1, "Preferences"); menu.addItem(2, "Edit Plugins"); menu.addSeparator(); + menu.addSectionHeader("Active Plugins"); // Active plugins int time = 0; for (int i = 0; i < activePluginList.getNumTypes(); i++) { PopupMenu options; - options.addItem(i+3, "Edit"); - options.addItem(activePluginList.getNumTypes()+i+3, "Delete"); - // TODO bypass + options.addItem(INDEX_EDIT + i, "Edit"); + std::vector timeSorted = getTimeSortedList(); + String key = getKey("bypass", timeSorted[i]); + bool bypass = getAppProperties().getUserSettings()->getBoolValue(key); + options.addItem(INDEX_BYPASS + i, "Bypass", true, bypass); + options.addSeparator(); + options.addItem(INDEX_MOVE_UP + i, "Move Up", i > 0); + options.addItem(INDEX_MOVE_DOWN + i, "Move Down", i < timeSorted.size() - 1); + options.addSeparator(); + options.addItem(INDEX_DELETE + i, "Delete"); PluginDescription plugin = getNextPluginOlderThanTime(time); menu.addSubMenu(plugin.name, options); } menu.addSeparator(); + menu.addSectionHeader("Avaliable Plugins"); // All plugins knownPluginList.addToMenu(menu, pluginSortMethod); } else { menu.addItem(1, "Quit"); + menu.addSeparator(); + menu.addItem(2, "Delete Plugin States"); + #if !JUCE_MAC + menu.addItem(3, "Invert Icon Color"); + #endif } #if JUCE_MAC || JUCE_LINUX menu.showMenuAsync(PopupMenu::Options().withTargetComponent(this), ModalCallbackFunction::forComponent(menuInvocationCallback, this)); @@ -261,10 +306,24 @@ void IconMenu::mouseDown(const MouseEvent& e) void IconMenu::menuInvocationCallback(int id, IconMenu* im) { // Right click - if ((!im->menuIconLeftClicked) && id == 1) + if ((!im->menuIconLeftClicked)) { - im->savePluginStates(); - return JUCEApplication::getInstance()->quit(); + if (id == 1) + { + im->savePluginStates(); + return JUCEApplication::getInstance()->quit(); + } + if (id == 2) + { + im->deletePluginStates(); + return im->loadActivePlugins(); + } + if (id == 3) + { + String color = getAppProperties().getUserSettings()->getValue("icon"); + getAppProperties().getUserSettings()->setValue("icon", color.equalsIgnoreCase("black") ? "white" : "black"); + return im->setIcon(); + } } #if JUCE_MAC // Click elsewhere @@ -281,27 +340,42 @@ void IconMenu::menuInvocationCallback(int id, IconMenu* im) if (id > 2) { // Delete plugin - if (id > im->activePluginList.getNumTypes() + 2 && id <= im->activePluginList.getNumTypes() * 2 + 2) + if (id >= im->INDEX_DELETE && id < im->INDEX_DELETE + 1000000) { im->deletePluginStates(); - int index = id - im->activePluginList.getNumTypes() - 3; - PluginDescription plugin = *im->activePluginList.getType(index); - String key = "pluginOrder-" + plugin.descriptiveName + plugin.version + plugin.pluginFormatName; + int index = id - im->INDEX_DELETE; + std::vector timeSorted = im->getTimeSortedList(); + String key = getKey("order", timeSorted[index]); + int unsortedIndex = 0; + for (int i = 0; im->activePluginList.getNumTypes(); i++) + { + PluginDescription current = *im->activePluginList.getType(i); + if (key.equalsIgnoreCase(getKey("order", current))) + { + unsortedIndex = i; + break; + } + } + + // Remove plugin order getAppProperties().getUserSettings()->removeValue(key); + // Remove bypass entry + getAppProperties().getUserSettings()->removeValue(getKey("bypass", timeSorted[index])); getAppProperties().saveIfNeeded(); - im->activePluginList.removeType(index); + + // Remove plugin from list + im->activePluginList.removeType(unsortedIndex); + // Save current states im->savePluginStates(); im->loadActivePlugins(); } // Add plugin - else if (id > im->activePluginList.getNumTypes() + 2) + else if (im->knownPluginList.getIndexChosenByMenu(id) > -1) { - im->deletePluginStates(); - PluginDescription plugin = *im->knownPluginList.getType(im->knownPluginList.getIndexChosenByMenu(id)); - String key = "pluginOrder-" + plugin.descriptiveName + plugin.version + plugin.pluginFormatName; + String key = getKey("order", plugin); int t = time(0); getAppProperties().getUserSettings()->setValue(key, t); getAppProperties().saveIfNeeded(); @@ -310,24 +384,88 @@ void IconMenu::menuInvocationCallback(int id, IconMenu* im) im->savePluginStates(); im->loadActivePlugins(); } + // Bypass plugin + else if (id >= im->INDEX_BYPASS && id < im->INDEX_BYPASS + 1000000) + { + int index = id - im->INDEX_BYPASS; + std::vector timeSorted = im->getTimeSortedList(); + String key = getKey("bypass", timeSorted[index]); + + // Set bypass flag + bool bypassed = getAppProperties().getUserSettings()->getBoolValue(key); + getAppProperties().getUserSettings()->setValue(key, !bypassed); + getAppProperties().saveIfNeeded(); + + im->savePluginStates(); + im->loadActivePlugins(); + } // Show active plugin GUI - else + else if (id >= im->INDEX_EDIT && id < im->INDEX_EDIT + 1000000) { - if (const AudioProcessorGraph::Node::Ptr f = im->graph.getNodeForId(id)) + if (const AudioProcessorGraph::Node::Ptr f = im->graph.getNodeForId(id - im->INDEX_EDIT + 1)) if (PluginWindow* const w = PluginWindow::getWindowFor(f, PluginWindow::Normal)) w->toFront(true); } + // Move plugin up the list + else if (id >= im->INDEX_MOVE_UP && id < im->INDEX_MOVE_UP + 1000000) + { + im->savePluginStates(); + std::vector timeSorted = im->getTimeSortedList(); + PluginDescription toMove = timeSorted[id - im->INDEX_MOVE_UP]; + for (int i = 0; i < timeSorted.size(); i++) + { + bool move = getKey("move", toMove).equalsIgnoreCase(getKey("move", timeSorted[i])); + getAppProperties().getUserSettings()->setValue(getKey("order", timeSorted[i]), move ? i : i+1); + if (move) + getAppProperties().getUserSettings()->setValue(getKey("order", timeSorted[i-1]), i+1); + } + im->loadActivePlugins(); + } + // Move plugin down the list + else if (id >= im->INDEX_MOVE_DOWN && id < im->INDEX_MOVE_DOWN + 1000000) + { + im->savePluginStates(); + std::vector timeSorted = im->getTimeSortedList(); + PluginDescription toMove = timeSorted[id - im->INDEX_MOVE_DOWN]; + for (int i = 0; i < timeSorted.size(); i++) + { + bool move = getKey("move", toMove).equalsIgnoreCase(getKey("move", timeSorted[i])); + getAppProperties().getUserSettings()->setValue(getKey("order", timeSorted[i]), move ? i+2 : i+1); + if (move) + { + getAppProperties().getUserSettings()->setValue(getKey("order", timeSorted[i + 1]), i + 1); + i++; + } + } + im->loadActivePlugins(); + } // Update menu im->startTimer(50); } } +std::vector IconMenu::getTimeSortedList() +{ + int time = 0; + std::vector list; + for (int i = 0; i < activePluginList.getNumTypes(); i++) + list.push_back(getNextPluginOlderThanTime(time)); + return list; + +} + +String IconMenu::getKey(String type, PluginDescription plugin) +{ + String key = "plugin-" + type.toLowerCase() + "-" + plugin.name + plugin.version + plugin.pluginFormatName; + return key; +} + void IconMenu::deletePluginStates() { + std::vector list = getTimeSortedList(); for (int i = 0; i < activePluginList.getNumTypes(); i++) { - String pluginUid; - pluginUid << "pluginState-" << i; + String pluginUid = getKey("state", list[i]); getAppProperties().getUserSettings()->removeValue(pluginUid); getAppProperties().saveIfNeeded(); } @@ -335,14 +473,14 @@ void IconMenu::deletePluginStates() void IconMenu::savePluginStates() { + std::vector list = getTimeSortedList(); for (int i = 0; i < activePluginList.getNumTypes(); i++) { - AudioProcessorGraph::Node* node = graph.getNodeForId(i+3); + AudioProcessorGraph::Node* node = graph.getNodeForId(i + 1); if (node == nullptr) break; AudioProcessor& processor = *node->getProcessor(); - String pluginUid; - pluginUid << "pluginState-" << i; + String pluginUid = getKey("state", list[i]); MemoryBlock savedStateBinary; processor.getStateInformation(savedStateBinary); getAppProperties().getUserSettings()->setValue(pluginUid, savedStateBinary.toBase64Encoding()); diff --git a/Source/IconMenu.hpp b/Source/IconMenu.hpp index ada8d31..f16c0ed 100644 --- a/Source/IconMenu.hpp +++ b/Source/IconMenu.hpp @@ -19,6 +19,9 @@ class IconMenu : public SystemTrayIconComponent, private Timer, public ChangeLis void mouseDown(const MouseEvent&); static void menuInvocationCallback(int id, IconMenu*); void changeListenerCallback(ChangeBroadcaster* changed); + static String getKey(String type, PluginDescription plugin); + + const int INDEX_EDIT, INDEX_BYPASS, INDEX_DELETE, INDEX_MOVE_UP, INDEX_MOVE_DOWN; private: #if JUCE_MAC std::string exec(const char* cmd); @@ -31,6 +34,8 @@ class IconMenu : public SystemTrayIconComponent, private Timer, public ChangeLis void deletePluginStates(); PluginDescription getNextPluginOlderThanTime(int &time); void removePluginsLackingInputOutput(); + std::vector getTimeSortedList(); + void setIcon(); AudioDeviceManager deviceManager; AudioPluginFormatManager formatManager; diff --git a/readme.md b/readme.md index 59e533a..4e6edb0 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,12 @@ Light Host --- -A simple VST/AU host for OS X, Windows, and Linux that sits in the menubar. +A simple VST/AU host for OS X, Windows, and Linux that sits in the menu/task bar. ### Features -- Add/remove plugins (AU/VST/VST3) -- Change output and input -- ASIO support for Windows -- Plugin states saved -- Saved plugin order +See #1 + +### Screenshot + +![Light Host 1.2](http://i.imgur.com/UF9SWfC.jpg) \ No newline at end of file