From 9c18f7ab7c508e135ca7896bcd903ef365bad1b8 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Tue, 16 Sep 2025 01:27:04 -0400 Subject: [PATCH] Plugin properties rework. --- .vscode/launch.json | 2 +- src/Pedalboard.cpp | 6 +- src/Pedalboard.hpp | 4 +- src/PiPedalModel.cpp | 4 +- src/PiPedalModel.hpp | 2 +- src/PiPedalSocket.cpp | 4 +- src/makeReleaseMain.cpp | 144 +- vite/src/pipedal/Draggable.tsx | 9 +- vite/src/pipedal/IconColorDropdownButton.tsx | 47 +- vite/src/pipedal/MainPage.tsx | 68 +- vite/src/pipedal/Pedalboard.tsx | 2 + vite/src/pipedal/PedalboardView.tsx | 1755 +++++++++--------- vite/src/pipedal/PerformanceView.tsx | 293 ++- vite/src/pipedal/PiPedalModel.tsx | 12 +- vite/src/pipedal/PluginControl.tsx | 30 +- vite/src/pipedal/PluginIcon.tsx | 64 +- vite/src/pipedal/PluginNameDialog.tsx | 190 ++ vite/src/pipedal/RenameDialog.tsx | 2 +- vite/src/pipedal/SelectHoverBackground.tsx | 1 + 19 files changed, 1603 insertions(+), 1036 deletions(-) create mode 100644 vite/src/pipedal/PluginNameDialog.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index de4a777..f149c6a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,7 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", "args": [ - "[ProfileThumbnails]" + "--gplot-downloads" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 590b27d..89324c9 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -153,12 +153,13 @@ bool Pedalboard::SetControlValue(int64_t pedalItemId, const std::string &symbol, return item->SetControlValue(symbol,value); } -bool Pedalboard::SetItemTitle(int64_t pedalItemId, const std::string &title) +bool Pedalboard::SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor) { PedalboardItem*item = GetItem(pedalItemId); if (!item) return false; - if (item->title() == title) return false; // no change. + if (item->title() == title && item->iconColor() == iconColor) return false; // no change. item->title(title); + item->iconColor(iconColor); return true; } @@ -532,6 +533,7 @@ JSON_MAP_BEGIN(PedalboardItem) JSON_MAP_REFERENCE(PedalboardItem,pathProperties) JSON_MAP_REFERENCE(PedalboardItem,title) JSON_MAP_REFERENCE(PedalboardItem,useModUi) + JSON_MAP_REFERENCE(PedalboardItem,iconColor) JSON_MAP_END() diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 4363d3c..995ddcb 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -106,6 +106,7 @@ public: std::map pathProperties_; std::string title_; bool useModUi_ = false; + std::string iconColor_; // non persistent state. PropertyMap patchProperties; @@ -135,6 +136,7 @@ public: GETTER_SETTER(stateUpdateCount) GETTER_SETTER_REF(lv2State) GETTER_SETTER_REF(title) + GETTER_SETTER_REF(iconColor) GETTER_SETTER(useModUi) Lv2PluginState&lv2State() { return lv2State_; } // non-const version. @@ -232,7 +234,7 @@ public: static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); - bool SetItemTitle(int64_t pedalItemId, const std::string &title); + bool SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor); bool SetItemEnabled(int64_t pedalItemId, bool enabled); bool SetItemUseModUi(int64_t pedalItemId, bool enabled); void SetCurrentSnapshotModified(bool modified); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index b36ba98..4adee31 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -3226,10 +3226,10 @@ void PiPedalModel::MoveAudioFile( AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); dir->MoveAudioFile(directory, fromPosition, toPosition); } -void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title) +void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string &colorKey) { std::lock_guard lock(mutex); - if (!this->pedalboard.SetItemTitle(instanceId, title)) + if (!this->pedalboard.SetItemTitle(instanceId, title, colorKey)) { return; } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index ebf3eae..012a49e 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -486,7 +486,7 @@ namespace pipedal int32_t from, int32_t to); - void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); + void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string&iconColor); void SetTone3000Auth(const std::string &apiKey); bool HasTone3000Auth() const; diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 83d1bfe..600b17b 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -179,12 +179,14 @@ class SetPedalboardItemTitleBody public: uint64_t instanceId_; std::string title_; + std::string colorKey_; DECLARE_JSON_MAP(SetPedalboardItemTitleBody); }; JSON_MAP_BEGIN(SetPedalboardItemTitleBody) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, instanceId) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title) +JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, colorKey) JSON_MAP_END() @@ -1569,7 +1571,7 @@ public: { SetPedalboardItemTitleBody body; pReader->read(&body); - model.SetPedalboardItemTitle(body.instanceId_, body.title_); + model.SetPedalboardItemTitle(body.instanceId_, body.title_, body.colorKey_); } else if (message == "getPatchProperty") { diff --git a/src/makeReleaseMain.cpp b/src/makeReleaseMain.cpp index 1459955..5e1e149 100644 --- a/src/makeReleaseMain.cpp +++ b/src/makeReleaseMain.cpp @@ -43,10 +43,11 @@ std::string psystem(const std::string &command) { throw std::runtime_error("popen() failed!"); } - Finally ff{[pipe]() { - pclose(pipe); - }}; - while (fgets(buffer.data(), buffer.size()-1, pipe) != nullptr) + Finally ff{[pipe]() + { + pclose(pipe); + }}; + while (fgets(buffer.data(), buffer.size() - 1, pipe) != nullptr) { result += buffer.data(); } @@ -68,7 +69,8 @@ void SignPackage() { packagePath = SS("build/pipedal_" << PROJECT_VER << "_amd64.deb"); packagePath = fs::absolute(packagePath); - if (!fs::exists(packagePath)) { + if (!fs::exists(packagePath)) + { packagePath = SS("build/pipedal_" << PROJECT_VER << "_*.deb"); throw std::runtime_error(SS("File does not exist: " << packagePath)); } @@ -82,8 +84,8 @@ void SignPackage() std::string signCmd = SS("/usr/bin/gpg --pinentry-mode loopback --yes --default-key " << UPDATE_GPG_FINGERPRINT2 - << " --armor --output " << packagePath << ".asc" - << " --detach-sign " << packagePath.c_str()); + << " --armor --output " << packagePath << ".asc" + << " --detach-sign " << packagePath.c_str()); int result = system(signCmd.c_str()); if (result != EXIT_SUCCESS) { @@ -226,6 +228,8 @@ public: bool draft = false, prerelease = false; std::vector assets; std::string published_at; + double release_downloads = 0; + double average_daily_downloads = 0; }; static std::string timePointToISO8601(const std::chrono::system_clock::time_point &tp) @@ -233,11 +237,33 @@ static std::string timePointToISO8601(const std::chrono::system_clock::time_poin auto tt = std::chrono::system_clock::to_time_t(tp); std::tm tm = *std::gmtime(&tt); std::stringstream ss; - ss << std::put_time(&tm,"%Y-%m-%dT%H:%M:%S") << "Z"; + ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S") << "Z"; return ss.str(); - } +static std::chrono::system_clock::time_point iso8601ToTimePoint(const std::string&date) +{ + std::istringstream ss(date); + std::tm tm = {}; + char zulu; + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S") >> zulu; + if (ss.fail()) { + throw std::runtime_error("Failed to parse ISO8601 date: " + date); + } + return std::chrono::system_clock::from_time_t(std::mktime(&tm)); +} + + + + +static bool endsWith(const std::string &value, const std::string &end) +{ + if (end.length() > value.length()) + { + return false; + } + return value.compare(value.length() - end.length(), end.length(), end) == 0; +} void GetDownloadCounts(bool gplotDownloads) { // error handling and temporary file use is different enough that it justifies @@ -321,15 +347,65 @@ void GetDownloadCounts(bool gplotDownloads) return left.published_at < right.published_at; // date ascending }); - for (size_t i = 0; i < releases.size()-1; ++i) + uint64_t cumulativeCount = 0; + + for (auto i = releases.begin(); i != releases.end(); /**/) { - releases[i].published_at = releases[i+1].published_at; + if (i->name.find("Experimental") != string::npos + || i->name.find("Retracted") != string::npos + || i->name.find("Testing") != string::npos + ) + { + i = releases.erase(i); + } + else + { + ++i; + } + } + for (auto &release : releases) + { + size_t releaseDownloads = 0; + for (const auto &asset : release.assets) + { + if (asset.name.ends_with(".deb")) + { + releaseDownloads += asset.downloads; + } + } + release.release_downloads = releaseDownloads; + } + + + + for (size_t i = 0; i < releases.size(); ++i) + { + std::chrono::system_clock::time_point nextTime; + if (i == releases.size()-1) + { + nextTime = std::chrono::system_clock::now(); + } else { + nextTime = iso8601ToTimePoint(releases.at(i+1).published_at); + } + auto thisTime = iso8601ToTimePoint(releases.at(i).published_at); + + auto seconds = std::chrono::duration_cast(nextTime-thisTime).count(); + auto days = seconds/(60.0*60*24); + + + releases[i].average_daily_downloads = releases[i].release_downloads/ + (days); + } + + for (size_t i = 0; i < releases.size() - 1; ++i) + { + releases[i].published_at = releases[i + 1].published_at; } if (releases.size() >= 1) { - releases[releases.size()-1].published_at = timePointToISO8601(std::chrono::system_clock::now()); + releases[releases.size() - 1].published_at = timePointToISO8601(std::chrono::system_clock::now()); } - uint64_t cumulativeCount = 0; + for (const auto &release : releases) { for (const auto &asset : release.assets) @@ -339,7 +415,10 @@ void GetDownloadCounts(bool gplotDownloads) cumulativeCount += asset.downloads; } } - cout << release.published_at << " " << cumulativeCount << endl; + cout << release.published_at << " " << cumulativeCount + << " " << release.average_daily_downloads + << " " << "\"" << release.name << "\"" + << endl; } } else @@ -351,14 +430,49 @@ void GetDownloadCounts(bool gplotDownloads) { return left.published_at > right.published_at; // latest date first. }); + + size_t total = 0; + size_t amdTotal = 0; + size_t amdAscTotal = 0; + size_t aarch64Total = 0; + size_t aarch64AscTotal = 0; + size_t otherTotal = 0; + for (const auto &release : releases) { - cout << release.name << endl; + cout << release.name << " " << release.published_at << endl; for (const auto &asset : release.assets) { + total += asset.downloads; + if (endsWith(asset.name, "arm64.deb")) + { + aarch64Total += asset.downloads; + } + else if (endsWith(asset.name, "amd64.deb")) + { + amdTotal += asset.downloads; + } + else if (endsWith(asset.name, "arm64.deb.asc")) + { + aarch64AscTotal += asset.downloads; + } + else if (endsWith(asset.name, "amd64.deb.asc")) + { + amdAscTotal += asset.downloads; + } + else + { + otherTotal += asset.downloads; + } cout << " " << asset.name << ": " << asset.downloads << endl; } } + cout << endl + << "Total: " << total << endl; + + cout << "arm64:" << aarch64Total << " asc: " << aarch64AscTotal << endl; + cout << "amd64:" << amdTotal << " asc: " << amdAscTotal << endl; + cout << "other:" << otherTotal << endl; } } } diff --git a/vite/src/pipedal/Draggable.tsx b/vite/src/pipedal/Draggable.tsx index ca2931e..7857efe 100644 --- a/vite/src/pipedal/Draggable.tsx +++ b/vite/src/pipedal/Draggable.tsx @@ -27,7 +27,7 @@ import { PiPedalStateError } from './PiPedalError'; import { css } from '@emotion/react'; -const SELECT_SCALE = 1.5; +const SELECT_SCALE = 1.2; const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_THRESHOLD = 48; @@ -58,6 +58,7 @@ export interface DraggableProps extends WithStyles { children?: ReactNode | ReactNode[]; draggable?: boolean; + style?: React.CSSProperties; } @@ -149,6 +150,7 @@ const Draggable = startClientY: number = 0; dragStarted: boolean = false; savedIndex: string = ""; + savedOpacity: string = ""; lastPointerDown: number = 0; originalBounds?: DOMRect; captureElement?: HTMLDivElement; @@ -244,6 +246,8 @@ const Draggable = this.pointerId = e.pointerId; this.pointerType = e.pointerType; this.savedIndex = e.currentTarget.style.zIndex; + this.savedOpacity = e.currentTarget.style.opacity; + e.currentTarget.style.opacity = "0.9"; if (this.pointerType !== "touch") { this.dragTarget.style.transform = "scale(" + SELECT_SCALE + ")"; this.dragTarget.style.zIndex = "3"; @@ -254,6 +258,7 @@ const Draggable = clearDragTransform(element: HTMLDivElement): void { element.style.transform = ""; element.style.zIndex = this.savedIndex; + element.style.opacity = this.savedOpacity; } onPointerCancel(e: PointerEvent) { @@ -486,7 +491,7 @@ const Draggable = render() { const classes = withStyles.getClasses(this.props); return ( -
void; dropdownAlignment: DropdownAlignment; @@ -71,19 +73,30 @@ const IconColorDropdownButton: React.FC = (props:
-
- + {props.pluginType ? ( +
+ +
+ ) : ( +
+ + )} +
@@ -93,20 +106,20 @@ const IconColorDropdownButton: React.FC = (props: onClose={handleClose} anchorOrigin={{ vertical: 'bottom', - horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left') + horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left') }} transformOrigin={{ vertical: 'top', - horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left'), + horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left'), }} > -
+
- handleColorSelect({key: "default", value: undefined})} + handleColorSelect({ key: "default", value: undefined })} style={{ width: 48, height: 48, borderRadius: 6, margin: 4, borderStyle: "solid", - borderWidth: colorKey ==="default" ? 2 : 0.25, + borderWidth: colorKey === "default" ? 2 : 0.25, borderColor: colorKey == "default" ? theme.palette.text.primary : theme.palette.text.secondary, diff --git a/vite/src/pipedal/MainPage.tsx b/vite/src/pipedal/MainPage.tsx index e7ba524..56eca58 100644 --- a/vite/src/pipedal/MainPage.tsx +++ b/vite/src/pipedal/MainPage.tsx @@ -24,7 +24,7 @@ import { withStyles } from "tss-react/mui"; import IconButtonEx from './IconButtonEx'; import ButtonEx from './ButtonEx'; import ButtonBase from '@mui/material/ButtonBase'; -import RenameDialog from './RenameDialog'; +import PluginIcon, { getIconColor } from './PluginIcon'; import ToolTipEx from './ToolTipEx'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; @@ -64,9 +64,11 @@ import PipedalUiIcon from './svg/pp_ui.svg?react'; import SnapshotDialog from './SnapshotDialog'; import { css } from '@emotion/react'; import { setDefaultModGuiPreference } from './ModGuiHost'; +import PluginNameDialog from './PluginNameDialog'; const SPLIT_CONTROLBAR_THRESHHOLD = 750; +const SHOW_ICON_THRESHHOLD = 475; const DISPLAY_AUTHOR_THRESHHOLD = 750; const DISPLAY_AUTHOR_SPLIT_THRESHOLD = 500; const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500; @@ -128,6 +130,7 @@ interface MainState { splitControlBar: boolean; displayAuthor: boolean; horizontalScrollLayout: boolean; + canDisplayPluginIcon: boolean; showMidiBindingsDialog: boolean; screenHeight: number; displayNameDialogOpen: boolean; @@ -147,6 +150,9 @@ export const MainPage = getSplitToolbar() { return this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD && this.windowSize.height >= HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK; } + getShowPluginIcon() { + return this.windowSize.width > SHOW_ICON_THRESHHOLD; + } getDisplayAuthor() { if (this.getSplitToolbar()) { return this.windowSize.width >= DISPLAY_AUTHOR_SPLIT_THRESHOLD; @@ -173,6 +179,7 @@ export const MainPage = addMenuAnchorEl: null, splitControlBar: this.getSplitToolbar(), displayAuthor: this.getDisplayAuthor(), + canDisplayPluginIcon: this.getShowPluginIcon(), horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, showMidiBindingsDialog: false, screenHeight: this.windowSize.height, @@ -304,6 +311,7 @@ export const MainPage = this.setState({ splitControlBar: this.getSplitToolbar(), displayAuthor: this.getDisplayAuthor(), + canDisplayPluginIcon: this.getShowPluginIcon(), horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, screenHeight: this.windowSize.height }); @@ -415,6 +423,7 @@ export const MainPage = return pedalboardItem.uri; } titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode { + let uiPlugin = pedalboardItem ? this.model.getUiPlugin(pedalboardItem?.uri) : null; let title = ""; let author = ""; let infoPluginUri = ""; @@ -489,21 +498,45 @@ export const MainPage = this.handleEditPluginDisplayName(); }} > -
- - {title} - {this.state.displayAuthor && ( - {author} - )} - +
+ {this.state.canDisplayPluginIcon && uiPlugin && ( +
+ +
+ )} +
+ + {title} + {this.state.displayAuthor && ( + {author} + )} + +
) : ( -
- {title} - {this.state.displayAuthor && ( - {author} +
+ {this.state.canDisplayPluginIcon && uiPlugin && ( +
+ +
)} +
+ + {title} + {this.state.displayAuthor && ( + {author} + )} + +
)} @@ -770,17 +803,14 @@ export const MainPage = this.onSnapshotDialogOk()} /> )} {(this.state.displayNameDialogOpen) && ( - this.setState({ displayNameDialogOpen: false })} - onOk={(newName: string) => { + onApply={(newName: string, color: string) => { if (pedalboardItem) { - this.model.setPedalboardItemTitle(pedalboardItem.instanceId, newName); + this.model.setPedalboardItemTitle(pedalboardItem.instanceId, newName, color); } - this.setState({ displayNameDialogOpen: false }); }} /> )} diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 25d9951..157a5e4 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -80,6 +80,7 @@ export class PedalboardItem implements Deserializable { this.lilvPresetUri = input.lilvPresetUri; this.pathProperties = input.pathProperties; this.useModUi = input.useModUi ?? false; + this.iconColor = input.iconColor??""; return this; } @@ -215,6 +216,7 @@ export class PedalboardItem implements Deserializable { lilvPresetUri: string = ""; pathProperties: {[Name: string]: string} = {}; useModUi: boolean = false; // true if this item should use the mod-ui. + iconColor: string = ""; }; export class SnapshotValue { diff --git a/vite/src/pipedal/PedalboardView.tsx b/vite/src/pipedal/PedalboardView.tsx index 8e3f29c..319cf2a 100644 --- a/vite/src/pipedal/PedalboardView.tsx +++ b/vite/src/pipedal/PedalboardView.tsx @@ -19,9 +19,9 @@ import React, { ReactNode, Component, SyntheticEvent } from 'react'; import { css } from '@emotion/react'; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; -import WithStyles, {withTheme} from './WithStyles'; +import WithStyles, { withTheme } from './WithStyles'; import { withStyles } from "tss-react/mui"; @@ -30,7 +30,7 @@ import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PluginType } from './Lv2Plugin'; import ButtonBase from '@mui/material/ButtonBase'; import Typography from '@mui/material/Typography'; -import PluginIcon, { SelectIconUri } from './PluginIcon'; +import PluginIcon, { getIconColor, SelectIconUri } from './PluginIcon'; import { SelectHoverBackground } from './SelectHoverBackground'; import SvgPathBuilder from './SvgPathBuilder'; import Draggable from './Draggable' @@ -42,9 +42,9 @@ import { Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType } from './Pedalboard'; -import MidiIcon from './svg/ic_midi.svg?react'; -import { midiChannelBindingControlFeatureEnabled } from './MidiChannelBinding'; - +// import MidiIcon from './svg/ic_midi.svg?react'; +// import { midiChannelBindingControlFeatureEnabled } from './MidiChannelBinding'; +// import CloseIcon from '@mui/icons-material/Close'; const START_CONTROL = Pedalboard.START_CONTROL; const END_CONTROL = Pedalboard.END_CONTROL; @@ -161,7 +161,24 @@ const pedalboardStyles = (theme: Theme) => createStyles({ marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2, width: FRAME_SIZE, height: FRAME_SIZE, - border: isDarkMode() ? "1pt #AAA solid" : "1pt #666 solid", + border: isDarkMode() ? "1pt #555 solid" : "1pt #666 solid", + borderRadius: 6 + }), + selectedIconFrame: css({ + + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "relative", + + background: theme.palette.background.default, + marginLeft: (CELL_WIDTH - FRAME_SIZE) / 2, + marginRight: (CELL_WIDTH - FRAME_SIZE) / 2, + marginTop: (CELL_HEIGHT - FRAME_SIZE) / 2, + marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2, + width: FRAME_SIZE, + height: FRAME_SIZE, + border: isDarkMode() ? "1pt #FFF solid" : "1pt #333 solid", borderRadius: 6 }), borderlessIconFrame: css({ @@ -245,6 +262,7 @@ class PedalLayout { name: string = ""; pluginType: PluginType = PluginType.Plugin; iconUrl: string = ""; + iconColor: string = ""; bounds: Rect = new Rect(); @@ -333,6 +351,7 @@ class PedalLayout { pluginType = PluginType.NamPlugin; } this.iconUrl = SelectIconUri(pluginType); + this.iconColor = pedalItem.iconColor; this.name = uiPlugin.label; if (pedalItem.title !== "") { this.name = pedalItem.title; @@ -401,908 +420,932 @@ class LayoutParams { const PedalboardView = -withTheme( -withStyles( - class extends Component { - model: PiPedalModel; + withTheme( + withStyles( + class extends Component { + model: PiPedalModel; - frameRef: React.RefObject; - scrollRef: React.RefObject; - private bgColor: string; + frameRef: React.RefObject; + scrollRef: React.RefObject; + private bgColor: string; - constructor(props: PedalboardProps) { - super(props); - this.model = PiPedalModelFactory.getInstance(); + constructor(props: PedalboardProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); - this.bgColor = props.theme.palette.background.default + this.bgColor = props.theme.palette.background.default - if (!props.selectedId) props.selectedId = -1; - this.state = { - pedalboard: this.model.pedalboard.get(), - }; - this.onPedalboardChanged = this.onPedalboardChanged.bind(this); - this.frameRef = React.createRef(); - this.scrollRef = React.createRef(); - this.handleTouchStart = this.handleTouchStart.bind(this); - } + if (!props.selectedId) props.selectedId = -1; + this.state = { + pedalboard: this.model.pedalboard.get(), + }; + this.onPedalboardChanged = this.onPedalboardChanged.bind(this); + this.frameRef = React.createRef(); + this.scrollRef = React.createRef(); + this.handleTouchStart = this.handleTouchStart.bind(this); + } - handleTouchStart(e: any) { - // just has to exist to allow Draggable to receive - // touchyMove. :-/ - } + handleTouchStart(e: any) { + // just has to exist to allow Draggable to receive + // touchyMove. :-/ + } - onDragEnd(instanceId: number, clientX: number, clientY: number) { - if (!this.props.enableStructureEditing) { - return; - } - if (!this.currentLayout) return; - - if (!this.frameRef.current) return; - - let currentLayout: PedalLayout[] = this.currentLayout; - let frameElement = this.frameRef.current; - - let rc = frameElement.getBoundingClientRect(); - clientX -= rc.left; - clientY -= rc.top; - - let it = chainIterator(currentLayout); - - while (true) { - let v = it.next(); - if (v.done) break; - let item = v.value; - - if (item.isSplitter() && item.pedalItem) { - if (item.bounds.contains(clientX, clientY)) { - if (clientX < item.bounds.x + CELL_WIDTH / 2) { - this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); - this.setSelection(instanceId); - return; - } else if (clientX > item.bounds.right - CELL_WIDTH / 2) { - this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); - this.setSelection(instanceId); - return; - - } - } - let yMid = (item.bounds.y + item.bounds.bottom) / 2; - if (clientX >= item.bounds.x - && clientY < yMid && clientY >= item.topChildren[0].bounds.y - - ) { - if (clientX < item.topChildren[0].bounds.x) { - let topPedalItem = item.topChildren[0].pedalItem; - if (topPedalItem) { - this.model.movePedalboardItemBefore(instanceId, topPedalItem.instanceId); - this.setSelection(instanceId); - return; - } - } - let lastTop = item.topChildren[item.topChildren.length - 1]; - if (clientX >= lastTop.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { - if (lastTop.pedalItem) { - this.model.movePedalboardItemAfter(instanceId, lastTop.pedalItem.instanceId); - this.setSelection(instanceId); - return; - } - } - - } - if (clientX >= item.bounds.x - && clientY > yMid && clientY < item.bottomChildren[0].bounds.bottom - ) { - if (clientX < item.bottomChildren[0].bounds.x) { - let bottomPedalItem = item.bottomChildren[0].pedalItem; - if (bottomPedalItem) { - this.model.movePedalboardItemBefore(instanceId, bottomPedalItem.instanceId); - this.setSelection(instanceId); - return; - } - } - let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; - if (clientX >= lastBottom.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { - if (lastBottom.pedalItem) { - this.model.movePedalboardItemAfter(instanceId, lastBottom.pedalItem.instanceId); - this.setSelection(instanceId); - return; - } - - } - - } - - } else if (item.bounds.contains(clientX, clientY)) { - if (item.isStart()) { - this.model.movePedalboardItemToStart(instanceId); - this.setSelection(instanceId); + onDragEnd(instanceId: number, clientX: number, clientY: number) { + if (!this.props.enableStructureEditing) { return; - } else if (item.isEnd()) { - this.model.movePedalboardItemToEnd(instanceId); - this.setSelection(instanceId); - return; - } else { - if (item.pedalItem) { - let margin = (CELL_WIDTH - FRAME_SIZE) / 2; - if (clientX < item.bounds.x + margin) { - this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); - } else if (clientX > item.bounds.right - margin) { - this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); + } + if (!this.currentLayout) return; + + if (!this.frameRef.current) return; + + let currentLayout: PedalLayout[] = this.currentLayout; + let frameElement = this.frameRef.current; + + let rc = frameElement.getBoundingClientRect(); + clientX -= rc.left; + clientY -= rc.top; + + let it = chainIterator(currentLayout); + + while (true) { + let v = it.next(); + if (v.done) break; + let item = v.value; + + if (item.isSplitter() && item.pedalItem) { + if (item.bounds.contains(clientX, clientY)) { + if (clientX < item.bounds.x + CELL_WIDTH / 2) { + this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); + this.setSelection(instanceId); + return; + } else if (clientX > item.bounds.right - CELL_WIDTH / 2) { + this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); + this.setSelection(instanceId); + return; + + } + } + let yMid = (item.bounds.y + item.bounds.bottom) / 2; + if (clientX >= item.bounds.x + && clientY < yMid && clientY >= item.topChildren[0].bounds.y + + ) { + if (clientX < item.topChildren[0].bounds.x) { + let topPedalItem = item.topChildren[0].pedalItem; + if (topPedalItem) { + this.model.movePedalboardItemBefore(instanceId, topPedalItem.instanceId); + this.setSelection(instanceId); + return; + } + } + let lastTop = item.topChildren[item.topChildren.length - 1]; + if (clientX >= lastTop.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { + if (lastTop.pedalItem) { + this.model.movePedalboardItemAfter(instanceId, lastTop.pedalItem.instanceId); + this.setSelection(instanceId); + return; + } + } + + } + if (clientX >= item.bounds.x + && clientY > yMid && clientY < item.bottomChildren[0].bounds.bottom + ) { + if (clientX < item.bottomChildren[0].bounds.x) { + let bottomPedalItem = item.bottomChildren[0].pedalItem; + if (bottomPedalItem) { + this.model.movePedalboardItemBefore(instanceId, bottomPedalItem.instanceId); + this.setSelection(instanceId); + return; + } + } + let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; + if (clientX >= lastBottom.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { + if (lastBottom.pedalItem) { + this.model.movePedalboardItemAfter(instanceId, lastBottom.pedalItem.instanceId); + this.setSelection(instanceId); + return; + } + + } + + } + + } else if (item.bounds.contains(clientX, clientY)) { + if (item.isStart()) { + this.model.movePedalboardItemToStart(instanceId); + this.setSelection(instanceId); + return; + } else if (item.isEnd()) { + this.model.movePedalboardItemToEnd(instanceId); + this.setSelection(instanceId); + return; } else { - this.model.movePedalboardItem(instanceId, item.pedalItem.instanceId); + if (item.pedalItem) { + let margin = (CELL_WIDTH - FRAME_SIZE) / 2; + if (clientX < item.bounds.x + margin) { + this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); + } else if (clientX > item.bounds.right - margin) { + this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); + } else { + this.model.movePedalboardItem(instanceId, item.pedalItem.instanceId); + } + this.setSelection(instanceId); + return; + } } - this.setSelection(instanceId); - return; + + } + } + // delete the plugin. + let newId = this.model.setPedalboardItemEmpty(instanceId); + this.setSelection(newId); + + } + + onPedalboardChanged(value?: Pedalboard) { + this.setState({ + pedalboard: value, + }); + } + + componentDidMount() { + this.scrollRef.current!.addEventListener("touchstart", this.handleTouchStart, { passive: false }); + this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged); + + } + componentWillUnmount() { + this.scrollRef.current!.removeEventListener("touchstart", this.handleTouchStart); + this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged); + } + + offsetLayout_(layoutItems: PedalLayout[], offset: number): void { + for (let i = 0; i < layoutItems.length; ++i) { + let layoutItem = layoutItems[i]; + layoutItem.bounds.y += offset; + if (layoutItem.isSplitter()) { + layoutItem.topConnectorY += offset; + layoutItem.bottomConnectorY += offset; + this.offsetLayout_(layoutItem.topChildren, offset); + this.offsetLayout_(layoutItem.bottomChildren, offset); + } + } + } + + getSplitterIcon(layoutItem: PedalLayout): PluginType { + if (layoutItem.pedalItem === undefined) { + throw new Error("Invalid splitter"); + } + let split = layoutItem.pedalItem as PedalboardSplitItem; + if (split.getSplitType() === SplitType.Ab) { + if (split.isASelected()) { + return PluginType.SplitA; + } else { + return PluginType.SplitB; + + } + } else if (split.getSplitType() === SplitType.Mix) { + return PluginType.SplitMix; //"img/fx_dial.svg"; + } else { + return PluginType.SplitLR; //"img/fx_lr.svg"; + } + } + + doLayout2_(lp: LayoutParams, layoutItems: PedalLayout[]): Rect { + let bounds = new Rect(); + for (let i = 0; i < layoutItems.length; ++i) { + let layoutItem = layoutItems[i]; + if (layoutItem.isSplitter()) { + let x0 = lp.cx; + let y0 = lp.cy; + + layoutItem.bounds.x = x0; + layoutItem.bounds.y = y0; + layoutItem.bounds.height = CELL_HEIGHT; + layoutItem.bounds.width = CELL_WIDTH; + + lp.cx += CELL_WIDTH; + + + let topBounds = this.doLayout2_(lp, layoutItem.topChildren); + if (topBounds.isEmpty()) { + topBounds.x = lp.cx; + topBounds.width = 0; + topBounds.y = y0 - CELL_HEIGHT / 2; + topBounds.height = CELL_HEIGHT; + + } + + + let dyTop = (lp.cy + CELL_HEIGHT / 2) - (topBounds.y + topBounds.height); + + + + this.offsetLayout_(layoutItem.topChildren, dyTop); + topBounds.offset(0, dyTop); + bounds.accumulate(topBounds); + + let topCx = lp.cx; + lp.cx = x0; + lp.cx += CELL_WIDTH; + + let bottomBounds = this.doLayout2_(lp, layoutItem.bottomChildren); + if (bottomBounds.isEmpty()) { + bottomBounds.x = lp.cx; bottomBounds.width = 0; + bottomBounds.y = lp.cy; bottomBounds.height = CELL_HEIGHT; + } + + let dyBottom = (lp.cy + CELL_HEIGHT / 2) - bottomBounds.y; + this.offsetLayout_(layoutItem.bottomChildren, dyBottom) + bottomBounds.offset(0, dyBottom); + bounds.accumulate(bottomBounds); + + lp.cx = Math.max(lp.cx, topCx) + CELL_WIDTH; + lp.cy = y0; + + layoutItem.bounds.width = lp.cx - layoutItem.bounds.x; + bounds.accumulate(layoutItem.bounds); + + if (layoutItem.topChildren.length === 0) { + layoutItem.topConnectorY = bounds.y + CELL_HEIGHT / 2; + } else { + layoutItem.topConnectorY = layoutItem.topChildren[0].bounds.y + CELL_HEIGHT / 2; + } + if (layoutItem.bottomChildren.length === 0) { + layoutItem.bottomConnectorY = bounds.y + bounds.height - CELL_HEIGHT / 2; + } else { + layoutItem.bottomConnectorY = layoutItem.bottomChildren[0].bounds.y + CELL_HEIGHT / 2; + } + } else { + layoutItem.bounds.x = lp.cx; + layoutItem.bounds.y = lp.cy; + lp.cx += CELL_WIDTH; + layoutItem.bounds.width = CELL_WIDTH; + layoutItem.bounds.height = CELL_HEIGHT; + bounds.accumulate(layoutItem.bounds); + + } + } + return bounds; + } + doLayout(layoutItems: PedalLayout[]): LayoutSize { + const TWO_ROW_HEIGHT = 142 - 14; + + if (layoutItems.length === 0) { + // if the current pedalboard is empty, reserve display space anyway. + return { width: 1, height: TWO_ROW_HEIGHT }; + } + + let lp = new LayoutParams(); + + let bounds = this.doLayout2_(lp, layoutItems); + // shift everything down so there are no negative y coordinates. + + if (bounds.height < TWO_ROW_HEIGHT) { + + let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height)) / 2); + this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra / 2)); + bounds.height += extra; + + } else { + this.offsetLayout_(layoutItems, -bounds.y); + } + + bounds.height += 14; // for labels that aren't accounted for. + return { width: bounds.width, height: bounds.height }; + + } + + onItemClick(e: SyntheticEvent, instanceId?: number): void { + if (instanceId) { + this.setSelection(instanceId); + } + } + setSelection(instanceId: number) { + if (this.props.onSelectionChanged) { + this.props.onSelectionChanged(instanceId); + } + } + + onItemDoubleClick(event: SyntheticEvent, instanceId?: number): void { + event.preventDefault(); + event.stopPropagation(); + + if (this.props.onDoubleClick && instanceId && this.props.enableStructureEditing) { + this.props.onDoubleClick(instanceId); + } + + } + + onItemLongClick(event: SyntheticEvent, instanceId?: number): void { + if (!instanceId) { + return; + } + event.preventDefault(); + event.stopPropagation(); + + if (!this.props.enableStructureEditing) { + this.setSelection(instanceId); + return; + } + if (!Utility.needsZoomedControls()) { + if (this.props.onDoubleClick && this.props.enableStructureEditing && instanceId) { + this.props.onDoubleClick(instanceId); } } } - } - // delete the plugin. - let newId = this.model.setPedalboardItemEmpty(instanceId); - this.setSelection(newId); - } - - onPedalboardChanged(value?: Pedalboard) { - this.setState({ - pedalboard: value, - }); - } - - componentDidMount() { - this.scrollRef.current!.addEventListener("touchstart", this.handleTouchStart, { passive: false }); - this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged); - - } - componentWillUnmount() { - this.scrollRef.current!.removeEventListener("touchstart", this.handleTouchStart); - this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged); - } - - offsetLayout_(layoutItems: PedalLayout[], offset: number): void { - for (let i = 0; i < layoutItems.length; ++i) { - let layoutItem = layoutItems[i]; - layoutItem.bounds.y += offset; - if (layoutItem.isSplitter()) { - layoutItem.topConnectorY += offset; - layoutItem.bottomConnectorY += offset; - this.offsetLayout_(layoutItem.topChildren, offset); - this.offsetLayout_(layoutItem.bottomChildren, offset); - } - } - } - - getSplitterIcon(layoutItem: PedalLayout): PluginType { - if (layoutItem.pedalItem === undefined) { - throw new Error("Invalid splitter"); - } - let split = layoutItem.pedalItem as PedalboardSplitItem; - if (split.getSplitType() === SplitType.Ab) { - if (split.isASelected()) { - return PluginType.SplitA; - } else { - return PluginType.SplitB; - - } - } else if (split.getSplitType() === SplitType.Mix) { - return PluginType.SplitMix; //"img/fx_dial.svg"; - } else { - return PluginType.SplitLR; //"img/fx_lr.svg"; - } - } - - doLayout2_(lp: LayoutParams, layoutItems: PedalLayout[]): Rect { - let bounds = new Rect(); - for (let i = 0; i < layoutItems.length; ++i) { - let layoutItem = layoutItems[i]; - if (layoutItem.isSplitter()) { - let x0 = lp.cx; - let y0 = lp.cy; - - layoutItem.bounds.x = x0; - layoutItem.bounds.y = y0; - layoutItem.bounds.height = CELL_HEIGHT; - layoutItem.bounds.width = CELL_WIDTH; - - lp.cx += CELL_WIDTH; - - - let topBounds = this.doLayout2_(lp, layoutItem.topChildren); - if (topBounds.isEmpty()) { - topBounds.x = lp.cx; - topBounds.width = 0; - topBounds.y = y0 - CELL_HEIGHT / 2; - topBounds.height = CELL_HEIGHT; + strokeConnector(output: ReactNode[], channels: number, enabled: Boolean, svgPath: string) { + let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + if (channels === 2) { + output.push(( + + )); + output.push(( + + )); + } else if (channels === 1) { + output.push(( + + )); } + } + renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { + // const classes = withStyles.getClasses(this.props); + let x_ = item.bounds.x + CELL_WIDTH / 2; + let y_ = item.bounds.y + CELL_HEIGHT / 2; + let numberOfOutputs = item.numberOfOutputs; + let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + let stereoCenterColor = this.bgColor; - let dyTop = (lp.cy + CELL_HEIGHT / 2) - (topBounds.y + topBounds.height); + if (item.originalInputs === 0) { + // break the input paths. + let rx = item.bounds.x + CELL_WIDTH / 2 - FRAME_SIZE / 2 - 4; + let ry = y_ - 4; - - - this.offsetLayout_(layoutItem.topChildren, dyTop); - topBounds.offset(0, dyTop); - bounds.accumulate(topBounds); - - let topCx = lp.cx; - lp.cx = x0; - lp.cx += CELL_WIDTH; - - let bottomBounds = this.doLayout2_(lp, layoutItem.bottomChildren); - if (bottomBounds.isEmpty()) { - bottomBounds.x = lp.cx; bottomBounds.width = 0; - bottomBounds.y = lp.cy; bottomBounds.height = CELL_HEIGHT; + output.push(( + + )); } + let svgPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_ + CELL_WIDTH, y_).toString(); - let dyBottom = (lp.cy + CELL_HEIGHT / 2) - bottomBounds.y; - this.offsetLayout_(layoutItem.bottomChildren, dyBottom) - bottomBounds.offset(0, dyBottom); - bounds.accumulate(bottomBounds); - lp.cx = Math.max(lp.cx, topCx) + CELL_WIDTH; - lp.cy = y0; - - layoutItem.bounds.width = lp.cx - layoutItem.bounds.x; - bounds.accumulate(layoutItem.bounds); - - if (layoutItem.topChildren.length === 0) { - layoutItem.topConnectorY = bounds.y + CELL_HEIGHT / 2; + if (numberOfOutputs === 2) { + output.push(( + + )); + output.push(( + + )); + } else if (numberOfOutputs === 1) { + output.push(( + + )); } else { - layoutItem.topConnectorY = layoutItem.topChildren[0].bounds.y + CELL_HEIGHT / 2; + output.push(( + + )); + } - if (layoutItem.bottomChildren.length === 0) { - layoutItem.bottomConnectorY = bounds.y + bounds.height - CELL_HEIGHT / 2; + } + renderSplitConnectors(output: ReactNode[], item: PedalLayout, enabled: boolean, shortSplitOutput: boolean,): void { + //const classes = withStyles.getClasses(this.props); + let x_ = item.bounds.x + CELL_WIDTH / 2; + let y_ = item.bounds.y + CELL_HEIGHT / 2; + let yTop = item.topConnectorY; + let yBottom = item.bottomConnectorY; + //let isStereo = item.stereoOutput; + let split = item.pedalItem as PedalboardSplitItem; + + let topEnabled = enabled && split.isASelected(); + let bottomEnabled = enabled && split.isBSelected(); + let topColor = topEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + let bottomColor = bottomEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + + + let topStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yTop).lineTo(x_ + CELL_WIDTH, yTop).toString(); + let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString(); + + if (item.numberOfInputs === 2 && item.topChildren[0].numberOfInputs === 2) { + output.push(()); + output.push(()); + } else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) { + output.push(()); + } + + if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) { + output.push(()); + output.push(()); + + } else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) { + output.push(()); + } + + let lastTop = item.topChildren[item.topChildren.length - 1]; + let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; + + let xTop = lastTop.bounds.right - CELL_WIDTH / 2; + let xBottom = lastBottom.bounds.right - CELL_WIDTH / 2; + + let xEnd = shortSplitOutput ? item.bounds.right : item.bounds.right + CELL_WIDTH / 2; + let xTee0 = item.bounds.right - CELL_WIDTH / 2; + + let firstPath: string; // top or bottom depending on draw order. + let secondPath: string; // top or bottom depending on draw order. + + let firstPathStereo: boolean; + let secondPathStereo: boolean; + let firstPathAbsent: boolean; + let secondPathAbsent: boolean; + let firstPathEnabled: boolean; + let secondPathEnabled: boolean; + let xTee: number; + + let monoAdjustment = (STEREO_STROKE_WIDTH - STROKE_WIDTH) / 2; + + let bottomPathFirst = topEnabled && !bottomEnabled; + let topPathFirst = bottomEnabled && !topEnabled; + + + // Third case: L/R stereo output, when both outputs are mono, requires a third stroke. + let thirdPath: string | null = null; // for L/R stereo output (which can be stereo even if both outputs are mono) + let hasThirdPath = item.numberOfOutputs === 2 && (lastTop.numberOfOutputs !== 2) && (lastBottom.numberOfOutputs !== 2); + + if (hasThirdPath) { + firstPathStereo = false; + secondPathStereo = false; + firstPathAbsent = lastTop.numberOfOutputs === 0 || item.numberOfOutputs === 0; + secondPathAbsent = lastBottom.numberOfOutputs === 0 || item.numberOfOutputs === 0; + xTee = xTee0 - monoAdjustment; + firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); + + secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); + + hasThirdPath = true; + thirdPath = new SvgPathBuilder().moveTo(xTee0, y_).lineTo(xEnd, y_).toString(); + firstPathEnabled = bottomEnabled; + secondPathEnabled = topEnabled; + } else if (bottomPathFirst || (topEnabled && lastTop.numberOfOutputs === 2)) { + // draw the bottom path first. + firstPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; + secondPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; + firstPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; + secondPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; + + xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; + firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); + xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; + + secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); + firstPathEnabled = bottomEnabled; + secondPathEnabled = topEnabled; } else { - layoutItem.bottomConnectorY = layoutItem.bottomChildren[0].bounds.y + CELL_HEIGHT / 2; + // draw the top path first. + firstPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; + secondPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; + firstPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; + secondPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; + + xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; + firstPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); + + xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; + secondPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); + + firstPathEnabled = topEnabled; + secondPathEnabled = bottomEnabled; } - } else { - layoutItem.bounds.x = lp.cx; - layoutItem.bounds.y = lp.cy; - lp.cx += CELL_WIDTH; - layoutItem.bounds.width = CELL_WIDTH; - layoutItem.bounds.height = CELL_HEIGHT; - bounds.accumulate(layoutItem.bounds); + let firstPathColor = firstPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + let secondPathColor = secondPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + if (bottomPathFirst || topPathFirst) { + // display stereo strokes with cutoff line. + if (firstPathStereo) { + output.push(( + + )); + output.push(( + + )); + } else if (!firstPathAbsent) { + output.push(( + + )); + } + if (secondPathStereo) { + output.push(( + + )); + output.push(( + + )); + } else if (!secondPathAbsent) { + output.push(( + + )); + } + + } else { + // stereo strokes merge. + if (firstPathStereo) { + output.push(( + + )); + } else if (!firstPathAbsent) { + output.push(( + + )); + } + if (secondPathStereo) { + output.push(( + + )); + } else if (!secondPathAbsent) { + output.push(( + + )); + } + + // draw stereo inner lines. + if (firstPathStereo) { + output.push(( + + )); + } + if (secondPathStereo) { + output.push(( + + )); + + } + } + if (thirdPath != null) { + // stereo output of L/R splitter + output.push(( + + )); + output.push(( + + )); + + + } } - } - return bounds; - } - doLayout(layoutItems: PedalLayout[]): LayoutSize { - const TWO_ROW_HEIGHT = 142 - 14; - - if (layoutItems.length === 0) { - // if the current pedalboard is empty, reserve display space anyway. - return { width: 1, height: TWO_ROW_HEIGHT }; - } - - let lp = new LayoutParams(); - - let bounds = this.doLayout2_(lp, layoutItems); - // shift everything down so there are no negative y coordinates. - - if (bounds.height < TWO_ROW_HEIGHT) { - - let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height)) / 2); - this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra / 2)); - bounds.height += extra; - - } else { - this.offsetLayout_(layoutItems, -bounds.y); - } - - bounds.height += 14; // for labels that aren't accounted for. - return { width: bounds.width, height: bounds.height }; - - } - - onItemClick(e: SyntheticEvent, instanceId?: number): void { - if (instanceId) { - this.setSelection(instanceId); - } - } - setSelection(instanceId: number) { - if (this.props.onSelectionChanged) { - this.props.onSelectionChanged(instanceId); - } - } - - onItemDoubleClick(event: SyntheticEvent, instanceId?: number): void { - event.preventDefault(); - event.stopPropagation(); - - if (this.props.onDoubleClick && instanceId && this.props.enableStructureEditing) { - this.props.onDoubleClick(instanceId); - } - - } - - onItemLongClick(event: SyntheticEvent, instanceId?: number): void { - if (!instanceId) { - return; - } - event.preventDefault(); - event.stopPropagation(); - - if (!this.props.enableStructureEditing) { - this.setSelection(instanceId); - return; - } - if (!Utility.needsZoomedControls()) { - if (this.props.onDoubleClick && this.props.enableStructureEditing && instanceId) { - this.props.onDoubleClick(instanceId); - } - } - - } - - strokeConnector(output: ReactNode[], channels: number, enabled: Boolean, svgPath: string) { - let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - - if (channels === 2) { - output.push(( - - )); - output.push(( - - )); - } else if (channels === 1) { - output.push(( - - )); - } - } - - renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { - // const classes = withStyles.getClasses(this.props); - let x_ = item.bounds.x + CELL_WIDTH / 2; - let y_ = item.bounds.y + CELL_HEIGHT / 2; - let numberOfOutputs = item.numberOfOutputs; - let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - let stereoCenterColor = this.bgColor; - - if (item.originalInputs === 0) { - // break the input paths. - let rx = item.bounds.x + CELL_WIDTH / 2 - FRAME_SIZE / 2 - 4; - let ry = y_ - 4; - - output.push(( - - )); - } - let svgPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_ + CELL_WIDTH, y_).toString(); - - - if (numberOfOutputs === 2) { - output.push(( - - )); - output.push(( - - )); - } else if (numberOfOutputs === 1) { - output.push(( - - )); - } else { - output.push(( - - )); - - } - } - renderSplitConnectors(output: ReactNode[], item: PedalLayout, enabled: boolean, shortSplitOutput: boolean,): void { - //const classes = withStyles.getClasses(this.props); - let x_ = item.bounds.x + CELL_WIDTH / 2; - let y_ = item.bounds.y + CELL_HEIGHT / 2; - let yTop = item.topConnectorY; - let yBottom = item.bottomConnectorY; - //let isStereo = item.stereoOutput; - let split = item.pedalItem as PedalboardSplitItem; - - let topEnabled = enabled && split.isASelected(); - let bottomEnabled = enabled && split.isBSelected(); - let topColor = topEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - let bottomColor = bottomEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - - - let topStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yTop).lineTo(x_ + CELL_WIDTH, yTop).toString(); - let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString(); - - if (item.numberOfInputs === 2 && item.topChildren[0].numberOfInputs === 2) { - output.push(()); - output.push(()); - } else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) { - output.push(()); - } - - if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) { - output.push(()); - output.push(()); - - } else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) { - output.push(()); - } - - let lastTop = item.topChildren[item.topChildren.length - 1]; - let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; - - let xTop = lastTop.bounds.right - CELL_WIDTH / 2; - let xBottom = lastBottom.bounds.right - CELL_WIDTH / 2; - - let xEnd = shortSplitOutput ? item.bounds.right : item.bounds.right + CELL_WIDTH / 2; - let xTee0 = item.bounds.right - CELL_WIDTH / 2; - - let firstPath: string; // top or bottom depending on draw order. - let secondPath: string; // top or bottom depending on draw order. - - let firstPathStereo: boolean; - let secondPathStereo: boolean; - let firstPathAbsent: boolean; - let secondPathAbsent: boolean; - let firstPathEnabled: boolean; - let secondPathEnabled: boolean; - let xTee: number; - - let monoAdjustment = (STEREO_STROKE_WIDTH - STROKE_WIDTH) / 2; - - let bottomPathFirst = topEnabled && !bottomEnabled; - let topPathFirst = bottomEnabled && !topEnabled; - - - // Third case: L/R stereo output, when both outputs are mono, requires a third stroke. - let thirdPath: string | null = null; // for L/R stereo output (which can be stereo even if both outputs are mono) - let hasThirdPath = item.numberOfOutputs === 2 && (lastTop.numberOfOutputs !== 2) && (lastBottom.numberOfOutputs !== 2); - - if (hasThirdPath) { - firstPathStereo = false; - secondPathStereo = false; - firstPathAbsent = lastTop.numberOfOutputs === 0 || item.numberOfOutputs === 0; - secondPathAbsent = lastBottom.numberOfOutputs === 0 || item.numberOfOutputs === 0; - xTee = xTee0 - monoAdjustment; - firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); - - secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); - - hasThirdPath = true; - thirdPath = new SvgPathBuilder().moveTo(xTee0, y_).lineTo(xEnd, y_).toString(); - firstPathEnabled = bottomEnabled; - secondPathEnabled = topEnabled; - } else if (bottomPathFirst || (topEnabled && lastTop.numberOfOutputs === 2)) { - // draw the bottom path first. - firstPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; - secondPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; - firstPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; - secondPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; - - xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; - firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); - xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; - - secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); - firstPathEnabled = bottomEnabled; - secondPathEnabled = topEnabled; - } else { - // draw the top path first. - firstPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; - secondPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; - firstPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; - secondPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; - - xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; - firstPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); - - xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; - secondPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); - - firstPathEnabled = topEnabled; - secondPathEnabled = bottomEnabled; - } - let firstPathColor = firstPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - let secondPathColor = secondPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - - if (bottomPathFirst || topPathFirst) { - // display stereo strokes with cutoff line. - if (firstPathStereo) { - output.push(( - - )); - output.push(( - - )); - } else if (!firstPathAbsent) { - output.push(( - - )); - } - if (secondPathStereo) { - output.push(( - - )); - output.push(( - - )); - } else if (!secondPathAbsent) { - output.push(( - - )); + getScrollContainer() { + let el: HTMLElement | undefined | null = this.scrollRef.current; + // actually not here anymore. :-/ It has a reactive definition in MainPage.tsx now. + while (el) { + if (el.id === "pedalboardScroll") { + return el as HTMLDivElement; + } + el = el.parentElement; + } + throw new PiPedalStateError("scroll container not found."); } - } else { - // stereo strokes merge. - if (firstPathStereo) { - output.push(( - - )); - } else if (!firstPathAbsent) { - output.push(( - - )); - } - if (secondPathStereo) { - output.push(( - - )); - } else if (!secondPathAbsent) { - output.push(( - - )); - } + pedalButton( + instanceId: number, + iconType: PluginType, + iconColor: string, + draggable: boolean, + enabled: boolean, + hasBorder: boolean = true, + pluginNotFound: boolean, + hasMidiConnector: boolean + ) + : ReactNode { + const classes = withStyles.getClasses(this.props); + let frameStyle = classes.iconFrame; + if (!hasBorder) { + frameStyle = classes.borderlessIconFrame; + } else { + if (instanceId === this.props.selectedId) { + frameStyle = classes.selectedIconFrame; + } + } - // draw stereo inner lines. - if (firstPathStereo) { - output.push(( - - )); - } - if (secondPathStereo) { - output.push(( - - )); + return ( +
{ e.preventDefault(); }} + > + {/* {!enabled && ( +
+ +
- } - } - if (thirdPath != null) { - // stereo output of L/R splitter - output.push(( - - )); - output.push(( - - )); + )} */} - - } - } - getScrollContainer() { - let el: HTMLElement | undefined | null = this.scrollRef.current; - // actually not here anymore. :-/ It has a reactive definition in MainPage.tsx now. - while (el) { - if (el.id === "pedalboardScroll") { - return el as HTMLDivElement; - } - el = el.parentElement; - } - throw new PiPedalStateError("scroll container not found."); - } - - pedalButton( - instanceId: number, - iconType: PluginType, - draggable: boolean, - enabled: boolean, - hasBorder: boolean = true, - pluginNotFound: boolean, - hasMidiConnector: boolean - ) - : ReactNode { - const classes = withStyles.getClasses(this.props); - return ( -
{ e.preventDefault(); }}> - {hasMidiConnector && midiChannelBindingControlFeatureEnabled && ( -
- -
- - )} - - { this.onItemClick(e, instanceId); }} - onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }} - onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }} - > - - this.getScrollContainer()} - onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} + { this.onItemClick(e, instanceId); }} + onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }} + onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }} > - - - - -
- ); + + this.getScrollContainer()} + onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} + style={{ opacity: enabled ? 0.99 : 0.3 }} - } - renderConnectors(output: ReactNode[], layoutChain: PedalLayout[], enabled: boolean, shortSplitOutput: boolean): void { - let length = layoutChain.length - 1; - if (layoutChain.length > 0 && layoutChain[layoutChain.length - 1].isSplitter()) { - ++length; - } - for (let i = 0; i < length; ++i) { - let item = layoutChain[i]; - if (item.isSplitter()) { - let splitter = item.pedalItem as PedalboardSplitItem; - this.renderSplitConnectors(output, item, enabled, i === length - 1 && shortSplitOutput,); - this.renderConnectors(output, item.topChildren, enabled && splitter.isASelected(), false); - this.renderConnectors(output, item.bottomChildren, enabled && splitter.isBSelected(), false); + > +
+ +
- } else if (item.uri !== END_PEDALBOARD_ITEM_URI) { - this.renderConnector(output, item, enabled); +
+
+ +
+ ); } - } - } - renderConnectorFrame(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { - let outputs: ReactNode[] = []; - this.renderConnectors(outputs, layoutChain, true, false); - return ( -
- - - { - outputs - } - - - -
- ); - - } - - renderChain(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { - - const classes = withStyles.getClasses(this.props); - - let result: ReactNode[] = []; - - result.push(this.renderConnectorFrame(layoutChain, layoutSize)); - - let it = chainIterator(layoutChain); - while (true) { - let v = it.next(); - if (v.done) break; - let item = v.value; - switch (item.uri) { - case START_PEDALBOARD_ITEM_URI: - result.push(
-
- - {this.pedalButton(START_CONTROL, item.pluginType, false, true, false, false, false)} -
-
); - break; - case END_PEDALBOARD_ITEM_URI: - result.push(
-
- - {this.pedalButton(END_CONTROL, item.pluginType, false, true, false, false, false)} -
-
); - break; - default: + renderConnectors(output: ReactNode[], layoutChain: PedalLayout[], enabled: boolean, shortSplitOutput: boolean): void { + let length = layoutChain.length - 1; + if (layoutChain.length > 0 && layoutChain[layoutChain.length - 1].isSplitter()) { + ++length; + } + for (let i = 0; i < length; ++i) { + let item = layoutChain[i]; if (item.isSplitter()) { + let splitter = item.pedalItem as PedalboardSplitItem; + this.renderSplitConnectors(output, item, enabled, i === length - 1 && shortSplitOutput,); + this.renderConnectors(output, item.topChildren, enabled && splitter.isASelected(), false); + this.renderConnectors(output, item.bottomChildren, enabled && splitter.isBSelected(), false); - result.push(
-
- {this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), false, true, true, false, false)} -
-
); + } else if (item.uri !== END_PEDALBOARD_ITEM_URI) { + this.renderConnector(output, item, enabled); - } else { - result.push( -
- {item.name} -
- ) - let uiPlugin = this.model.getUiPlugin(item.pedalItem?.uri ?? ""); - let pluginMissing = uiPlugin === null; - let pluginType = item.pluginType; - if (uiPlugin && uiPlugin.uri === "http://two-play.com/plugins/toob-nam") - { - pluginType = PluginType.NamPlugin; + } + } + } + renderConnectorFrame(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { + let outputs: ReactNode[] = []; + this.renderConnectors(outputs, layoutChain, true, false); + return ( +
+ + + { + outputs + } + + +
+ ); + + } + + renderChain(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { + + const classes = withStyles.getClasses(this.props); + + let result: ReactNode[] = []; + + result.push(this.renderConnectorFrame(layoutChain, layoutSize)); + + let it = chainIterator(layoutChain); + while (true) { + let v = it.next(); + if (v.done) break; + let item = v.value; + switch (item.uri) { + case START_PEDALBOARD_ITEM_URI: + result.push(
+
+ + {this.pedalButton(START_CONTROL, item.pluginType, item.iconColor, false, true, false, false, false)} +
+
); + break; + case END_PEDALBOARD_ITEM_URI: + result.push(
+
+ + {this.pedalButton(END_CONTROL, item.pluginType, "", false, true, false, false, false)} +
+
); + break; + default: + if (item.isSplitter()) { + + result.push(
+
+ {this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), "", false, true, true, false, false)} +
+
); + + } else { + result.push( +
+ {item.name} +
+ ) + let uiPlugin = this.model.getUiPlugin(item.pedalItem?.uri ?? ""); + let pluginMissing = uiPlugin === null; + let pluginType = item.pluginType; + if (uiPlugin && uiPlugin.uri === "http://two-play.com/plugins/toob-nam") { + pluginType = PluginType.NamPlugin; + + } + + result.push(
+ {this.pedalButton( + item.pedalItem?.instanceId ?? -1, + pluginType, + item.pedalItem?.iconColor ?? "", + !item.isEmpty(), + item.pedalItem?.isEnabled ?? false, + true, + pluginMissing, + uiPlugin ? ((uiPlugin.has_midi_input !== 0) || (uiPlugin.has_midi_output !== 0)) : false)} + +
); + + } + break; + } + + } + return result; + } + + canInputStero(item: PedalLayout): boolean { + if (item.pedalItem) { + let plugin = this.model.getUiPlugin(item.pedalItem.uri); + if (plugin) { + return plugin.audio_inputs === 2; + } + } + return true; + } + getNumberOfInputs(item: PedalLayout): number { + if (item.pedalItem) { + let plugin = this.model.getUiPlugin(item.pedalItem.uri); + if (plugin) { + return plugin.audio_inputs; + } + } + return 1; + } + getNumberOfOutputs(item: PedalLayout): number { + if (item.pedalItem) { + let plugin = this.model.getUiPlugin(item.pedalItem.uri); + if (plugin) { + return plugin.audio_outputs; + } + } + return 1; + } + + markStereoOutputs(layoutChain: PedalLayout[], numberOfInputs: number, numberOfOutputs: number) { + // analyze forward flow. + this.markStereoForward(layoutChain, numberOfInputs); + // mark items that feed a mono effect as mono. + this.markStereoBackward(layoutChain, numberOfOutputs); + } + + markStereoBackward(layoutChain: PedalLayout[], numberOfOutputs: number): number { + for (let i = layoutChain.length - 1; i >= 0; --i) { + let item = layoutChain[i]; + if (item.isSplitter()) { + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs) + + this.markStereoBackward(item.topChildren, numberOfOutputs); + this.markStereoBackward(item.bottomChildren, numberOfOutputs); + let topInputs = item.topChildren[0].numberOfInputs; + let bottomInputs = item.bottomChildren[0].numberOfInputs; + + let splitItem = item.pedalItem as PedalboardSplitItem; + if (splitItem.getSplitType() !== SplitType.Lr) { + item.numberOfInputs = CalculateConnection(item.numberOfInputs, Math.max(topInputs, bottomInputs)); } + } else if (item.isEnd()) { - result.push(
- {this.pedalButton( - item.pedalItem?.instanceId ?? -1, - pluginType, - !item.isEmpty(), - item.pedalItem?.isEnabled ?? false, - true, - pluginMissing, - uiPlugin ? ((uiPlugin.has_midi_input !== 0) || (uiPlugin.has_midi_output !== 0)) : false)} + } else if (item.isStart()) { + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); + return item.numberOfOutputs; -
); - - } - break; - } - - } - return result; - } - - canInputStero(item: PedalLayout): boolean { - if (item.pedalItem) { - let plugin = this.model.getUiPlugin(item.pedalItem.uri); - if (plugin) { - return plugin.audio_inputs === 2; - } - } - return true; - } - getNumberOfInputs(item: PedalLayout): number { - if (item.pedalItem) { - let plugin = this.model.getUiPlugin(item.pedalItem.uri); - if (plugin) { - return plugin.audio_inputs; - } - } - return 1; - } - getNumberOfOutputs(item: PedalLayout): number { - if (item.pedalItem) { - let plugin = this.model.getUiPlugin(item.pedalItem.uri); - if (plugin) { - return plugin.audio_outputs; - } - } - return 1; - } - - markStereoOutputs(layoutChain: PedalLayout[], numberOfInputs: number, numberOfOutputs: number) { - // analyze forward flow. - this.markStereoForward(layoutChain, numberOfInputs); - // mark items that feed a mono effect as mono. - this.markStereoBackward(layoutChain, numberOfOutputs); - } - - markStereoBackward(layoutChain: PedalLayout[], numberOfOutputs: number): number { - for (let i = layoutChain.length - 1; i >= 0; --i) { - let item = layoutChain[i]; - if (item.isSplitter()) { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs) - - this.markStereoBackward(item.topChildren, numberOfOutputs); - this.markStereoBackward(item.bottomChildren, numberOfOutputs); - let topInputs = item.topChildren[0].numberOfInputs; - let bottomInputs = item.bottomChildren[0].numberOfInputs; - - let splitItem = item.pedalItem as PedalboardSplitItem; - if (splitItem.getSplitType() !== SplitType.Lr) { - item.numberOfInputs = CalculateConnection(item.numberOfInputs, Math.max(topInputs, bottomInputs)); - } - } else if (item.isEnd()) { - - } else if (item.isStart()) { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); - return item.numberOfOutputs; - - } else if (item.isEmpty()) { - if (numberOfOutputs === 0) { - item.numberOfOutputs = 0; - item.numberOfInputs = CalculateConnection(item.numberOfInputs, 2); - } else { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); - item.numberOfInputs = CalculateConnection(item.numberOfInputs, numberOfOutputs); - } - } else { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); - } - numberOfOutputs = item.numberOfInputs; - } - return numberOfOutputs; - } - markStereoForward(layoutChain: PedalLayout[], numberOfInputs: number): number { - if (layoutChain.length === 0) { - return numberOfInputs; - } - for (let i = 0; i < layoutChain.length; ++i) { - let item = layoutChain[i]; - if (item.isSplitter()) { - let splitter = item.pedalItem as PedalboardSplitItem; - item.numberOfInputs = numberOfInputs; - - let chainInputs = numberOfInputs; - if (splitter.getSplitType() === SplitType.Lr) { - chainInputs = CalculateConnection(numberOfInputs, 1); - } - let topOutputs = this.markStereoForward(item.topChildren, chainInputs); - let bottomOutputs = this.markStereoForward(item.bottomChildren, chainInputs); - - - if (splitter.getSplitType() === SplitType.Ab) { - if (splitter.isASelected()) { - item.numberOfOutputs = topOutputs; + } else if (item.isEmpty()) { + if (numberOfOutputs === 0) { + item.numberOfOutputs = 0; + item.numberOfInputs = CalculateConnection(item.numberOfInputs, 2); + } else { + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); + item.numberOfInputs = CalculateConnection(item.numberOfInputs, numberOfOutputs); + } } else { - item.numberOfOutputs = bottomOutputs; + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); } - } else { - item.numberOfOutputs = (topOutputs >= 1 || bottomOutputs >= 1) ? 2 : 1; - } - } else if (item.isStart()) { - item.numberOfOutputs = Math.min(PiPedalModelFactory.getInstance().jackSettings.get().inputAudioPorts.length, 2); - } else if (item.isEnd()) { - item.numberOfInputs = - CalculateConnection( - Math.min(PiPedalModelFactory.getInstance().jackSettings.get().outputAudioPorts.length, 2), - numberOfInputs); - return item.numberOfInputs; - } else if (item.isEmpty()) { - item.numberOfInputs = numberOfInputs; - if (numberOfInputs === 0) { - item.numberOfOutputs = 2; - } else { - item.numberOfOutputs = item.numberOfInputs; - } - } else { - if (item.numberOfInputs === 0) // zero-input plugins merge their output with the input. - { - item.numberOfInputs = numberOfInputs; - item.numberOfOutputs = Math.max(item.numberOfOutputs, numberOfInputs); - } else { - item.numberOfInputs = CalculateConnection(numberOfInputs, this.getNumberOfInputs(item)); - item.numberOfOutputs = this.getNumberOfOutputs(item); + numberOfOutputs = item.numberOfInputs; } + return numberOfOutputs; } - numberOfInputs = item.numberOfOutputs; - } - return numberOfInputs; - } + markStereoForward(layoutChain: PedalLayout[], numberOfInputs: number): number { + if (layoutChain.length === 0) { + return numberOfInputs; + } + for (let i = 0; i < layoutChain.length; ++i) { + let item = layoutChain[i]; + if (item.isSplitter()) { + let splitter = item.pedalItem as PedalboardSplitItem; + item.numberOfInputs = numberOfInputs; - currentLayout?: PedalLayout[]; - private renderKey: number = 0; - render() { - const classes = withStyles.getClasses(this.props); - this.renderKey = 0; - let layoutChain = makeChain(this.model, this.state.pedalboard?.items); - let start = PedalLayout.Start(); - let end = PedalLayout.End(); - if (layoutChain.length !== 0) { - layoutChain.splice(0, 0, start); - layoutChain.splice(layoutChain.length, 0, end); - this.markStereoOutputs(layoutChain, 2, 2); - } - - let layoutSize = this.doLayout(layoutChain); + let chainInputs = numberOfInputs; + if (splitter.getSplitType() === SplitType.Lr) { + chainInputs = CalculateConnection(numberOfInputs, 1); + } + let topOutputs = this.markStereoForward(item.topChildren, chainInputs); + let bottomOutputs = this.markStereoForward(item.bottomChildren, chainInputs); - this.currentLayout = layoutChain; // save for mouse processing &c. + if (splitter.getSplitType() === SplitType.Ab) { + if (splitter.isASelected()) { + item.numberOfOutputs = topOutputs; + } else { + item.numberOfOutputs = bottomOutputs; + } + } else { + item.numberOfOutputs = (topOutputs >= 1 || bottomOutputs >= 1) ? 2 : 1; + } + } else if (item.isStart()) { + item.numberOfOutputs = Math.min(PiPedalModelFactory.getInstance().jackSettings.get().inputAudioPorts.length, 2); + } else if (item.isEnd()) { + item.numberOfInputs = + CalculateConnection( + Math.min(PiPedalModelFactory.getInstance().jackSettings.get().outputAudioPorts.length, 2), + numberOfInputs); + return item.numberOfInputs; + } else if (item.isEmpty()) { + item.numberOfInputs = numberOfInputs; + if (numberOfInputs === 0) { + item.numberOfOutputs = 2; + } else { + item.numberOfOutputs = item.numberOfInputs; + } + } else { + if (item.numberOfInputs === 0) // zero-input plugins merge their output with the input. + { + item.numberOfInputs = numberOfInputs; + item.numberOfOutputs = Math.max(item.numberOfOutputs, numberOfInputs); + } else { + item.numberOfInputs = CalculateConnection(numberOfInputs, this.getNumberOfInputs(item)); + item.numberOfOutputs = this.getNumberOfOutputs(item); + } + } + numberOfInputs = item.numberOfOutputs; + } + return numberOfInputs; + } - return ( -
-
- {this.renderChain(layoutChain, layoutSize)} -
-
- ); - } + currentLayout?: PedalLayout[]; + private renderKey: number = 0; + render() { + const classes = withStyles.getClasses(this.props); + this.renderKey = 0; + let layoutChain = makeChain(this.model, this.state.pedalboard?.items); + let start = PedalLayout.Start(); + let end = PedalLayout.End(); + if (layoutChain.length !== 0) { + layoutChain.splice(0, 0, start); + layoutChain.splice(layoutChain.length, 0, end); + this.markStereoOutputs(layoutChain, 2, 2); + } - }, - pedalboardStyles -)); + let layoutSize = this.doLayout(layoutChain); + + + this.currentLayout = layoutChain; // save for mouse processing &c. + + return ( +
+
+ {this.renderChain(layoutChain, layoutSize)} +
+
+ ); + } + + }, + pedalboardStyles + )); export default PedalboardView \ No newline at end of file diff --git a/vite/src/pipedal/PerformanceView.tsx b/vite/src/pipedal/PerformanceView.tsx index c05142b..4e17a5b 100644 --- a/vite/src/pipedal/PerformanceView.tsx +++ b/vite/src/pipedal/PerformanceView.tsx @@ -21,7 +21,7 @@ import { Theme } from '@mui/material/styles'; import SaveIconOutline from '@mui/icons-material/Save'; import WithStyles from './WithStyles'; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; import { withStyles } from "tss-react/mui"; import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel'; @@ -41,7 +41,7 @@ import { BankIndex } from './Banks'; import SnapshotEditor from './SnapshotEditor'; import { Snapshot } from './Pedalboard'; import JackStatusView from './JackStatusView'; -import {IDialogStackable, popDialogStack, pushDialogStack} from './DialogStack'; +import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack'; import { css } from '@emotion/react'; @@ -75,7 +75,8 @@ interface PerformanceViewProps extends WithStyles { } interface PerformanceViewState { - wrapSelects: boolean + wrapSelects: boolean; + largeAppBar: boolean; presets: PresetIndex; banks: BankIndex; showSnapshotEditor: boolean; @@ -86,7 +87,7 @@ interface PerformanceViewState { export const PerformanceView = - withStyles( + withStyles( class extends ResizeResponsiveComponent implements IDialogStackable { model: PiPedalModel; @@ -100,6 +101,7 @@ export const PerformanceView = presets: this.model.presets.get(), banks: this.model.banks.get(), wrapSelects: false, + largeAppBar: this.updateAppBarZoom(), showSnapshotEditor: false, showStatusMonitor: this.model.showStatusMonitor.get(), snapshotEditorIndex: 0, @@ -112,6 +114,9 @@ export const PerformanceView = } + updateAppBarZoom() { + return this.windowSize.width > 700; + } showStatusMonitorHandler() { this.setState({ @@ -119,7 +124,7 @@ export const PerformanceView = }); } - getTag() { return "performView"} + getTag() { return "performView" } isOpen() { return this.props.open } onDialogStackClose() { this.props.onClose(); @@ -128,15 +133,13 @@ export const PerformanceView = private hasHooks: boolean = false; - updateHooks() : void { + updateHooks(): void { let wantHooks = this.mounted && this.props.open; - if (wantHooks !== this.hasHooks) - { + if (wantHooks !== this.hasHooks) { this.hasHooks = wantHooks; - - if (this.hasHooks) - { + + if (this.hasHooks) { pushDialogStack(this); } else { @@ -144,9 +147,12 @@ export const PerformanceView = } } } - + onWindowSizeChanged(width: number, height: number): void { - this.setState({ wrapSelects: width < 700 }); + this.setState({ + wrapSelects: width < 700, + largeAppBar: this.updateAppBarZoom(), + }); } onPresetsChanged(newValue: PresetIndex) { @@ -156,7 +162,7 @@ export const PerformanceView = this.setState({ banks: this.model.banks.get() }) } onPresetChangedChanged(newValue: boolean) { - this.setState({ presetModified:newValue}); + this.setState({ presetModified: newValue }); } private mounted: boolean = false; @@ -216,28 +222,23 @@ export const PerformanceView = }); return true; } - handleSnapshotEditOk(index: number, name: string, color: string,newSnapshots: (Snapshot|null)[]) - { + handleSnapshotEditOk(index: number, name: string, color: string, newSnapshots: (Snapshot | null)[]) { // results have been sent to the server. we would perform a short-cut commit to our local state, to avoid laggy display updates. // but we don't actually have that in our state. } - handlePreviousBank() - { + handlePreviousBank() { this.model.previousBank(); } - handleNextBank() - { + handleNextBank() { this.model.nextBank(); } - handlePreviousPreset() - { + handlePreviousPreset() { this.model.previousPreset(); } - handleNextPreset() - { + handleNextPreset() { this.model.nextPreset(); } @@ -246,54 +247,42 @@ export const PerformanceView = let wrapSelects = this.state.wrapSelects; let presets = this.state.presets; let banks = this.state.banks; + let appBarIconSize: "large" | undefined = this.state.largeAppBar ? undefined : "large"; return (
-
- { this.props.onClose(); }} - style={{ - position: "relative",top: 3, - flex: "0 0 auto" }} > - - - + {this.state.largeAppBar ? (
- {/********* BANKS *******************/} -
- { this.handlePreviousBank(); }} - size="medium" - color="inherit" - style={{ - borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 - }} - > - - + { this.props.onClose(); }} + style={{ + position: "relative", top: 3, + flex: "0 0 auto" + }} > + + + {/********* BANKS *******************/} +
- - { this.handleNextBank(); }} - color="inherit" - style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} - > - - - - {/** spacer */} { this.handleNextBank(); }} size="medium" color="inherit" - style={{visibility: "hidden"}} + style={{ visibility: "hidden" }} > - + - -
{/********* PRESETS *******************/} -
+
{ this.handlePreviousPreset(); }} color="inherit" style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }} > - + this.handleBankSelectClose(e)} + value={banks.selectedBank === 0 ? undefined : banks.selectedBank} + inputProps={{ + classes: { icon: classes.select_icon }, + 'aria-label': "Select preset" + }} + > + { + banks.entries.map((entry) => { + return ( + + {entry.name} + + ); + }) + } + + {/** spacer */} + { this.handleNextBank(); }} + size="medium" + color="inherit" + style={{ visibility: "hidden" }} + > + + + + + + +
+ {/********* PRESETS *******************/} +
+ { this.handlePreviousPreset(); }} + color="inherit" + style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }} + > + + + + + { this.handleNextPreset(); }} + color="inherit" + style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} + > + + + + { this.model.saveCurrentPreset(); }} + size="medium" + color="inherit" + style={{ flexShrink: 0, visibility: this.state.presetModified ? "visible" : "hidden" }} + > + + + +
+
+
+ + )} -
+
{ return this.handleOnEdit(index); }} />
@@ -413,15 +526,15 @@ export const PerformanceView = )}
{this.state.showSnapshotEditor && ( - { + { this.setState({ showSnapshotEditor: false }); }} - onOk={(index,name,color,newSnapshots)=>{ - this.setState({ showSnapshotEditor: false}); - this.handleSnapshotEditOk(index,name,color,newSnapshots); + onOk={(index, name, color, newSnapshots) => { + this.setState({ showSnapshotEditor: false }); + this.handleSnapshotEditOk(index, name, color, newSnapshots); }} - /> + /> )}
) diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index a42cef1..38e6b2b 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -491,6 +491,9 @@ export class PiPedalModel //implements PiPedalModel webSocket?: PiPedalSocket; + static getInstance(): PiPedalModel { + return PiPedalModelFactory.getInstance(); + } hasTone3000Auth: ObservableProperty = new ObservableProperty(false); canKeepScreenOn: boolean = false; @@ -3376,9 +3379,9 @@ export class PiPedalModel //implements PiPedalModel this.cancelOnNetworkChanging(); }, 30 * 1000); - } - setPedalboardItemTitle(instanceId: number, title: string): void { + + setPedalboardItemTitle(instanceId: number, title: string, iconColor: string): void { let pedalboard = this.pedalboard.get(); if (!pedalboard) { throw new PiPedalStateError("Pedalboard not loaded."); @@ -3386,13 +3389,14 @@ export class PiPedalModel //implements PiPedalModel let newPedalboard = pedalboard.clone(); this.updateVst3State(newPedalboard); let item = newPedalboard.getItem(instanceId); - if (item.title === title) { + if (item.title === title && item.iconColor === iconColor) { return; } item.title = title; + item.iconColor = iconColor; this.pedalboard.set(newPedalboard); // notify the server. - this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title }); + this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title, colorKey: iconColor }); } setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void { this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); diff --git a/vite/src/pipedal/PluginControl.tsx b/vite/src/pipedal/PluginControl.tsx index 3af35d4..80065cb 100644 --- a/vite/src/pipedal/PluginControl.tsx +++ b/vite/src/pipedal/PluginControl.tsx @@ -307,7 +307,7 @@ const PluginControl = e.stopPropagation(); } - isValidPointer(e: PointerEvent): boolean { + isValidPointer(e: PointerEvent): boolean { if (e.pointerType === "mouse") { return e.button === 0; } else if (e.pointerType === "pen") { @@ -347,7 +347,7 @@ const PluginControl = pointerId: number = 0; pointerType: string = ""; - isCapturedPointer(e: PointerEvent): boolean { + isCapturedPointer(e: PointerEvent): boolean { return this.mouseDown && e.pointerId === this.pointerId && e.pointerType === this.pointerType; @@ -361,7 +361,7 @@ const PluginControl = pointersDown: number = 0; captureElement?: SVGSVGElement = undefined; - onPointerDown(e: PointerEvent): void { + onPointerDown(e: PointerEvent): void { if (!this.mouseDown && this.isValidPointer(e)) { e.preventDefault(); e.stopPropagation(); @@ -387,7 +387,7 @@ const PluginControl = this.mouseDown = true; if (this.pointersDown === 1) { this.isTap = true; - this.tapStartMs = Date.now(); + this.tapStartMs = e.timeStamp; } else { this.isTap = false; } @@ -429,7 +429,7 @@ const PluginControl = } - onPointerLostCapture(e: PointerEvent) { + onPointerLostCapture(e: PointerEvent) { if (this.isCapturedPointer(e)) { if (this.pointersDown !== 0) { --this.pointersDown; @@ -442,7 +442,7 @@ const PluginControl = } - updateRange(e: PointerEvent): number { + updateRange(e: PointerEvent): number { let ultraHigh = false; let high = false; @@ -486,9 +486,11 @@ const PluginControl = } } } - onPointerTap() { - let tapTime = Date.now(); + onPointerTap(e: PointerEvent) { + let tapTime = e.timeStamp; let dT = tapTime - this.lastTapMs; + + this.lastTapMs = tapTime; if (dT < 500) { @@ -497,7 +499,7 @@ const PluginControl = } private tapStartMs: number = 0; - onPointerUp(e: PointerEvent) { + onPointerUp(e: PointerEvent) { if (this.isCapturedPointer(e)) { if (this.pointersDown !== 0) { @@ -514,9 +516,9 @@ const PluginControl = this.releaseCapture(e); if (this.isTap) { - let ms = Date.now() - this.tapStartMs; + let ms = e.timeStamp - this.tapStartMs; if (ms < 200) { - this.onPointerTap(); + this.onPointerTap(e); } } // prevent click from firing on other elements @@ -531,7 +533,7 @@ const PluginControl = } } - releaseCapture(e: PointerEvent) { + releaseCapture(e: PointerEvent) { let img = this.imgRef.current; if (img && img.style) { @@ -556,7 +558,7 @@ const PluginControl = clickSlop() { return 5; // maybe larger on touch devices. } - onPointerMove(e: PointerEvent): void { + onPointerMove(e: PointerEvent): void { if (this.isCapturedPointer(e)) { e.preventDefault(); let dRange = this.updateRange(e) @@ -1138,7 +1140,7 @@ const PluginControl = ); } /* - isSamePointer(PointerEvent e): boolean + isSamePointer(PointerEvent e): boolean { return e.pointerId === this.pointerId && e.pointerType === this.pointerType; diff --git a/vite/src/pipedal/PluginIcon.tsx b/vite/src/pipedal/PluginIcon.tsx index 82b8925..2365c2e 100644 --- a/vite/src/pipedal/PluginIcon.tsx +++ b/vite/src/pipedal/PluginIcon.tsx @@ -23,7 +23,6 @@ import WithStyles from './WithStyles'; import {createStyles} from './WithStyles'; import { withStyles } from "tss-react/mui"; -import { PiPedalModelFactory } from "./PiPedalModel"; import { PluginType } from './Lv2Plugin'; @@ -67,6 +66,7 @@ import FxEmptyIcon from './svg/fx_empty.svg?react'; import FxTerminalIcon from './svg/fx_terminal.svg?react'; import { isDarkMode } from './DarkMode'; +import { PiPedalModel } from './PiPedalModel'; export interface IconColorSelect { key: string, @@ -80,21 +80,65 @@ export const getIconColorSelections = (darkMode?: boolean): IconColorSelect[] => let l: number; let c: number; if (darkMode) { - l = 0.8; + l = 0.8 c = 0.25; } else { l = 0.5; - c = 0.25; + c = 0.20; } for (let angle = 0; angle < 360; angle += 22.5) { - result.push( - { - key: "oklch" + Math.round(angle*10).toString(), - value: ("oklch(" + l.toString() + " " + c.toString() + " " + (angle).toString() + ")") - + if (angle === 270-22.5) + { + // for some reason, this one is useless. Patch in a more useful light blue. :-/ + let a2 = 270; + let l2: number; + let c2: number; + if (darkMode) { + l2 = 0.9 + c2 = 0.25; + } else { + l2 = 0.7; + c2 = 0.20; } - ); + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l2.toString() + " " + c2.toString() + " " + (a2).toString() + ")") + + } + ); + + } else if (angle === 270-45) + { + // for some reason, this one is useless. Patch in a more useful light blue. :-/ + let a2 = 180; + let l2: number; + let c2: number; + if (darkMode) { + l2 = 0.95 + c2 = 0.25; + } else { + l2 = 0.75; + c2 = 0.20; + } + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l2.toString() + " " + c2.toString() + " " + (a2).toString() + ")") + + } + ); + + } else { + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l.toString() + " " + c.toString() + " " + (angle).toString() + ")") + + } + ); + } } return result; @@ -317,7 +361,7 @@ export function SelectBaseIcon(plugin_type: PluginType): string { export function SelectIconUri(plugin_type: PluginType) { let icon = SelectBaseIcon(plugin_type); - return PiPedalModelFactory.getInstance().svgImgUrl(icon); + return PiPedalModel.getInstance().svgImgUrl(icon); } const PluginIcon = withStyles((props: PluginIconProps) => { diff --git a/vite/src/pipedal/PluginNameDialog.tsx b/vite/src/pipedal/PluginNameDialog.tsx new file mode 100644 index 0000000..864d518 --- /dev/null +++ b/vite/src/pipedal/PluginNameDialog.tsx @@ -0,0 +1,190 @@ +// Copyright (c) Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER` +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React from 'react'; +import Typography from '@mui/material/Typography'; +import IconButtonEx from './IconButtonEx'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import IconColorDropdownButton, { DropdownAlignment } from './IconColorDropdownButton'; +import InputLabel from '@mui/material/InputLabel'; +import DialogEx from './DialogEx'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogContent'; +import { nullCast } from './Utility'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import { PiPedalModel } from './PiPedalModel'; +//import TextFieldEx from './TextFieldEx'; +import TextField from '@mui/material/TextField'; +import { PedalboardItem } from './Pedalboard'; + + +export interface PluginNameDialogProps { + open: boolean, + pedalboardItem: PedalboardItem | null, + allowEmpty?: boolean, + label?: string, + onApply: (text: string, color: string) => void, + onClose: () => void +}; + +export interface PluginNameDialogState { + iconColor: string; +}; + +function isTouchUi() { + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +} +export default class PluginNameDialog extends ResizeResponsiveComponent { + + refText: React.RefObject; + + constructor(props: PluginNameDialogProps) { + super(props); + this.state = { + iconColor: this.props.pedalboardItem?.iconColor ?? "" + }; + this.refText = React.createRef(); + } + mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void { + } + + + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + } + + componentDidUpdate() { + } + checkForIllegalCharacters(filename: string) { + if (filename.indexOf('/') !== -1) { + throw new Error("Illegal character: '/'"); + } + } + + render() { + let props = this.props; + let { open, onClose, onApply } = props; + let model = PiPedalModel.getInstance(); + + + if (!open) return null; + let pedalboardItem = props.pedalboardItem; + if (!pedalboardItem) return null; + + let uiPlugin = model.getUiPlugin(pedalboardItem.uri); + if (uiPlugin === null) { + return null; + } + let defaultName = props.pedalboardItem?.title ?? ""; + const handleClose = () => { + onClose(); + }; + + const handleOk = () => { + let text = nullCast(this.refText.current).value; + text = text.trim(); + try { + this.checkForIllegalCharacters(text); + } catch (e: any) { + let model: PiPedalModel = PiPedalModel.getInstance(); + model.showAlert(e.toString()); + return; + } + if (text.length === 0 && props.allowEmpty !== true) return; + onApply(text, this.state.iconColor); + onClose(); + } + const handleKeyDown = (event: React.KeyboardEvent): void => { + // 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleOk(); + } + }; + return ( + { }} + > + +
+ onClose()} aria-label="back" + > + + + + Plugin Properties + +
+
+ +
+ { this.props.onApply(e.target.value.toString(), this.state.iconColor) }} + autoCapitalize="off" + slotProps={{ + input: { + style: { scrollMargin: 24 } + }, + inputLabel: { + shrink: true + } + }} + id="name" + type="text" + label="Plugin Display name" + placeholder={uiPlugin?.label ?? ""} + defaultValue={defaultName} + inputRef={this.refText} + /> +
+ Icon color + { + if (this.props.pedalboardItem) { + this.setState({ iconColor: selection.key }); + onApply(this.props.pedalboardItem.title, selection.key); + } + }} + pluginType={uiPlugin.plugin_type} + dropdownAlignment={DropdownAlignment.SE} + /> +
+
+
+
+ ); + } +} diff --git a/vite/src/pipedal/RenameDialog.tsx b/vite/src/pipedal/RenameDialog.tsx index 48dadd6..06666a6 100644 --- a/vite/src/pipedal/RenameDialog.tsx +++ b/vite/src/pipedal/RenameDialog.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) Robin E. R. Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/vite/src/pipedal/SelectHoverBackground.tsx b/vite/src/pipedal/SelectHoverBackground.tsx index 060136c..bc482af 100644 --- a/vite/src/pipedal/SelectHoverBackground.tsx +++ b/vite/src/pipedal/SelectHoverBackground.tsx @@ -57,6 +57,7 @@ interface HoverProps extends WithStyles { selected: boolean; showHover?: boolean; borderRadius?: number; + clipChildren?: boolean; children?: React.ReactNode; }