looper/backends/ui/haiku/main_window.cpp
Zachary Hall 9899794b81
Some checks failed
Build / build-gentoo (push) Failing after 1m11s
Build / download-system-deps (push) Successful in 3m44s
Build / get-source-code (push) Successful in 13m23s
Build / build-deb (push) Successful in 11m40s
Build / build-appimage (push) Successful in 4m53s
Build / build-android (push) Failing after 3m19s
Build / build-windows (push) Failing after 8m9s
Add cat support
2024-12-21 14:23:00 -08:00

446 lines
15 KiB
C++

#include "main_window.h"
#include <Button.h>
#include <Slider.h>
#include <Message.h>
#include <GroupView.h>
#include <Messenger.h>
#include <MessageRunner.h>
#include <MenuBar.h>
#include <Path.h>
#include <Menu.h>
#include <MessageFilter.h>
#include <functional>
#include <util.hpp>
#include <MenuItem.h>
#include <thread>
#include <log.hpp>
#include <cmath>
#include <fmt/core.h>
#include <fmt/format.h>
#include <LayoutItem.h>
#include <TranslationUtils.h>
#include <SDL.h>
#include <cats.hpp>
#include "icons.h"
#include "utils.h"
#include "aboutwindow.h"
#define CMD_MOUSE_DOWN_KEY "catmeow:pressed"
#define CMD_PAUSE_RESUME 0x10
#define CMD_RESTART 0x11
#define CMD_STOP 0x12
#define CMD_OPEN 0x13
#define CMD_ABOUT 0x14
#define CMD_PREFS 0x15
#define CMD_SEEK 0x20
#define CMD_VOLUME 0x21
#define CMD_SPEED 0x22
#define CMD_PITCH 0x23
#define CMD_TEMPO 0x24
#define CMD_UPDATE 0x80
#define SLIDER_SCALE 1000
#define D(x) ((double)(x))
std::vector<Subwindow*> Subwindow::windows;
static BMessage *make_slider_msg(uint32_t what, bool down) {
BMessage *msg = new BMessage(what);
msg->SetBool(CMD_MOUSE_DOWN_KEY, down);
return msg;
}
static bool is_slider_down_msg(BMessage *msg) {
return msg->HasBool(CMD_MOUSE_DOWN_KEY) && msg->GetBool(CMD_MOUSE_DOWN_KEY);
}
void HaikuLooperWindow::UpdateViewFlags(BView *view) {
if (view == NULL) return;
view->SetFlags(view->Flags()|B_SUPPORTS_LAYOUT|B_FRAME_EVENTS);
view->SetResizingMode(B_FOLLOW_ALL_SIDES);
}
void HaikuLooperWindow::UpdateViewFlags(BLayout *layout) {
BView *owner = layout->Owner();
if (owner == NULL) {
owner = layout->View();
if (owner == NULL) {
return;
}
}
UpdateViewFlags(owner);
}
void HaikuLooperWindow::AddCat(std::string name, BBitmap *bitmap) {
if (bitmap == NULL) return;
if (cats.contains(name)) {
delete cats[name];
}
cats[name] = bitmap;
}
void HaikuLooperWindow::LoadCat(const char *path) {
return BTranslationUtils::LoadBitmap(path);
}
void HaikuLooperWindow::LoadCat(const void *ptr, size_t len, const char *name) {
BMemoryIO *io = new BMemoryIO(ptr, len);
BBitmap *output = BTranslationUtils::LoadBitmap(io);
delete io;
return output;
}
HaikuLooperWindow::HaikuLooperWindow(Playback *playback) : BWindow(BRect(100, 100, 500, 100), "Looper", B_TITLED_WINDOW, 0) {
pause_bitmap = load_icon(ICON_PAUSE);
resume_bitmap = load_icon(ICON_PLAY);
stop_bitmap = load_icon(ICON_STOP);
refresh_bitmap = load_icon(ICON_REFRESH);
ref_handler = new HaikuLooperRefHandler(this);
this->playback = playback;
float minW, minH, maxW, maxH;
GetSizeLimits(&minW, &maxW, &minH, &maxH);
minW = 600;
minH = 200;
SetSizeLimits(minW, maxW, minH, maxH);
ResizeTo(minW, minH);
DisableUpdates();
auto about_window = new HaikuAboutWindow();
layout = new BGroupLayout(B_VERTICAL);
SetLayout(layout);
layout->SetSpacing(0.0);
layout->SetInsets(0, 0, 0, 0);
BMenuBar *menu_bar = new BMenuBar("main_menu");
file_menu = new BMenu("File");
open_item = new BMenuItem("Open...", new BMessage(CMD_OPEN));
prefs_item = new BMenuItem("Preferences...", new BMessage(CMD_PREFS));
quit_item = new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED));
help_menu = new BMenu("Help");
about_item = new BMenuItem("About...", new BMessage(CMD_ABOUT));
file_menu->AddItem(open_item);
file_menu->AddItem(prefs_item);
file_menu->AddItem(quit_item);
help_menu->AddItem(about_item);
menu_bar->AddItem(file_menu);
menu_bar->AddItem(help_menu);
layout->AddView(menu_bar);
spacer = new BView("spacer", B_SUPPORTS_LAYOUT|B_FRAME_EVENTS);
spacer->SetExplicitPreferredSize(BSize(0, 0));
spacer->SetExplicitMinSize(BSize(0, 0));
layout->AddView(spacer);
BGroupView *top_row = new BGroupView(B_HORIZONTAL);
BGroupView *bottom_row = new BGroupView(B_HORIZONTAL);
BLayoutItem *top_item = layout->AddView(top_row);
BLayoutItem *bottom_item = layout->AddView(bottom_row);
pause_resume_btn = new BButton(NULL, new BMessage(CMD_PAUSE_RESUME));
pause_resume_btn->SetTarget(this);
top_row->AddChild(pause_resume_btn);
restart_btn = new BButton(NULL, new BMessage(CMD_RESTART));
restart_btn->SetTarget(this);
top_row->AddChild(restart_btn);
BView *slider_parent = new BView("main:slider_parent", B_SUPPORTS_LAYOUT);
BGroupLayout *slider_parent_layout = new BGroupLayout(B_HORIZONTAL, 0);
slider_parent_layout->SetInsets(0);
slider_parent->SetLayout(slider_parent_layout);
slider = new BSlider("seek_slider", "Seek", make_slider_msg(CMD_SEEK, false), 0, INT32_MAX, B_HORIZONTAL);
slider->SetModificationMessage(make_slider_msg(CMD_SEEK, true));
slider->SetTarget(this);
auto *slider_item = slider_parent_layout->AddView(slider);
slider_item->SetExplicitAlignment(BAlignment(B_ALIGN_USE_FULL_WIDTH, B_ALIGN_USE_FULL_HEIGHT));
position_label = new BStringView("main:position_label", "Stopped");
auto *pos_label_item = slider_parent_layout->AddView(position_label);
pos_label_item->SetExplicitAlignment(BAlignment(B_ALIGN_USE_FULL_WIDTH, B_ALIGN_VERTICAL_CENTER));
position_label->Show();
auto *slider_parent_item = top_row->GroupLayout()->AddView(slider_parent);
slider_parent_item->SetExplicitAlignment(BAlignment(B_ALIGN_USE_FULL_WIDTH, B_ALIGN_USE_FULL_HEIGHT));
slider->Hide();
stop_btn = new BButton(NULL, new BMessage(CMD_STOP));
stop_btn->SetTarget(this);
top_row->AddChild(stop_btn);
volume_slider = new HaikuLooperSlider("volume", "Volume", new BMessage(CMD_VOLUME), 0, 0, 100, 1, false);
volume_slider->SetLimitLabels("Muted", "Full Volume");
volume_slider->SetTarget(this);
top_row->AddChild(volume_slider);
speed_slider = new HaikuLooperSlider("speed", "Speed", new BMessage(CMD_SPEED), 0, 0.25, 4.0, 0.01, true);
speed_slider->SetLimitLabels("0.25x", "4.00x");
speed_slider->SetTarget(this);
bottom_row->AddChild(speed_slider);
tempo_slider = new HaikuLooperSlider("tempo", "Tempo", new BMessage(CMD_TEMPO), 0, 0.25, 4.0, 0.01, true);
tempo_slider->SetLimitLabels("0.25x", "4.00x");
tempo_slider->SetTarget(this);
bottom_row->AddChild(tempo_slider);
pitch_slider = new HaikuLooperSlider("pitch", "Pitch", new BMessage(CMD_PITCH), 0, 0.25, 4.0, 0.01, true);
pitch_slider->SetLimitLabels("0.25x", "4.00x");
pitch_slider->SetTarget(this);
bottom_row->AddChild(pitch_slider);
file_panel = new BFilePanel(B_OPEN_PANEL, new BMessenger(this));
auto prefs_window = new HaikuPrefsWindow(this);
prefs_subwindow = Subwindow::Add(prefs_window);
about_subwindow = Subwindow::Add(about_window);
EnableUpdates();
InvalidateLayout(true);
BRect top_rect = top_item->Frame();
top_item->SetExplicitMinSize(top_rect.Size());
BRect bottom_rect = bottom_item->Frame();
bottom_item->SetExplicitMinSize(bottom_rect.Size());
Pulse();
this->update_thread = new std::thread(std::mem_fn(&HaikuLooperWindow::ThreadFunc), this);
UpdateIfNeeded();
StartWatching(prefs_window, CMD_UPDATE_LABEL_SETTING);
auto *msg = new BMessage(CMD_UPDATE_LABEL_SETTING);
MessageReceived(msg);
delete msg;
}
void HaikuLooperWindow::UpdateCat(BBitmap *cat) {
if (cat != NULL) {
auto bounds = cat->Bounds();
auto w = bounds.Width(), h = bounds.Height();
auto sb = spacer->Bounds();
auto sw = sb.Width(), sh = sb.Height();
BRect src(0, 0, w, h);
if (w > sw || h > sh) {
float aspect = w / h;
if (w > sw) {
w = sw;
h = sw / aspect;
}
if (h > sh) {
h = sh;
w = sh * aspect;
}
}
BRect dst(sw - w, sh - h, w, h);
spacer->SetOverlayBitmap(cat, src, dst, NULL, B_FOLLOW_RIGHT|B_FOLLOW_BOTTOM);
} else {
spacer->ClearOverlayBitmap();
}
}
HaikuLooperWindow::~HaikuLooperWindow() {
delete ref_handler;
delete pause_bitmap;
delete resume_bitmap;
delete stop_bitmap;
delete refresh_bitmap;
playback->Stop();
done = true;
update_thread->join();
delete update_thread;
}
void HaikuLooperWindow::MessageReceived(BMessage *msg) {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_DROPFILE) {
playback->Start(ev.drop.file);
} else if (ev.type == SDL_QUIT) {
Hide();
return;
}
}
if (msg->what == B_REFS_RECEIVED) {
entry_ref ref;
if (msg->FindRef("refs", &ref) != B_OK) { // Don't crash when failing to get ref - This is probably an error in the system or another application
return;
}
BEntry entry(&ref, true);
BPath path;
if (entry.GetPath(&path) != B_OK) { // Don't know how this could cause an error, but don't crash if it does.
return;
}
playback->Start(path.Path());
return;
}
if (msg->IsSystem()) {
switch (msg->what) {
default: {
msg->PrintToStream();
} break;
}
return;
}
switch (msg->what) {
case CMD_PAUSE_RESUME: {
playback->Pause();
} break;
case CMD_RESTART: {
playback->Seek(0.0);
} break;
case CMD_VOLUME: {
playback->SetVolume(msg->GetDouble("be:value", 100));
volume_clicked = is_slider_down_msg(msg);
} break;
case CMD_SPEED: {
playback->SetSpeed(msg->GetDouble("be:value", 1.0));
speed_clicked = is_slider_down_msg(msg);
} break;
case CMD_TEMPO: {
playback->SetTempo(msg->GetDouble("be:value", 1.0));
tempo_clicked = is_slider_down_msg(msg);
} break;
case CMD_PITCH: {
playback->SetPitch(msg->GetDouble("be:value", 1.0));
pitch_clicked = is_slider_down_msg(msg);
} break;
case CMD_SEEK: {
playback->Seek((((double)msg->GetInt32("be:value", 0)) / INT32_MAX) * playback->GetLength());
seek_clicked = is_slider_down_msg(msg);
} break;
case CMD_UPDATE: {
Pulse();
} break;
case CMD_STOP: {
playback->Stop();
} break;
case CMD_OPEN: {
file_panel->Show();
} break;
case CMD_ABOUT: {
about_subwindow->Show();
} break;
case CMD_PREFS: {
prefs_subwindow->Show();
} break;
case CMD_SET_CAT: {
BBitmap *cat = NULL;
if (msg->FindBitmap("cat", &cat) != B_OK) {
cat = NULL;
}
UpdateCat(cat);
}
case CMD_UPDATE_LABEL_SETTING: {
volume_slider->MessageReceived(msg);
speed_slider->MessageReceived(msg);
tempo_slider->MessageReceived(msg);
pitch_slider->MessageReceived(msg);
if (show_labels) {
stop_btn->SetLabel("Stop");
restart_btn->SetLabel("Restart");
} else {
stop_btn->SetLabel("");
restart_btn->SetLabel("");
}
if (show_icons) {
stop_btn->SetIcon(stop_bitmap);
restart_btn->SetIcon(refresh_bitmap);
} else {
stop_btn->SetIcon(get_empty_icon());
restart_btn->SetIcon(get_empty_icon());
}
} break;
case 'DATA': {
status_t err = B_OK;
entry_ref ref;
if ((err = msg->FindRef("refs", &ref)) == B_OK) {
BEntry entry(&ref);
BPath path;
if ((err = entry.GetPath(&path)) == B_OK) {
playback->Start(path.Path());
} else {
WARNING.writefln("entry.GetPath(&path) == %d", (int32)(err));
msg->PrintToStream();
}
} else {
WARNING.writefln("msg->FindRef(\"refs\", &ref) == %d", (int32)(err));
msg->PrintToStream();
}
} break;
default: {
if (msg->HasPointer("catmeow:target")) {
BHandler *ptr = (BHandler*)msg->GetPointer("catmeow:target");
ptr->MessageReceived(msg);
} else {
WARNING.writeln("Unrecognized message type");
msg->PrintToStream();
}
} break;
};
}
void HaikuLooperWindow::Pulse() {
auto len = playback->GetLength();
auto pos = playback->GetPosition();
auto pos_milliseconds = pos * 1000.0;
int32_t pos_int32 = (int32_t)((((int64_t)pos_milliseconds) * INT32_MAX) / 1000 / len);
if (!seek_clicked) slider->SetValue(pos_int32);
auto component_count = TimeToComponentCount(len);
bool enable_ui = !playback->IsStopped();
if (enable_ui) {
if (len <= 0.0) {
position_label->SetText(fmt::format("Position: {} units", (int)pos).c_str());
slider->Hide();
position_label->Show();
} else {
slider->SetLabel(fmt::format("Position: {}", TimeToString(pos, component_count)).c_str());
slider->SetLimitLabels(TimeToString(0, component_count).c_str(), TimeToString(len, component_count).c_str());
position_label->Hide();
slider->Show();
}
if (show_icons) pause_resume_btn->SetIcon(playback->IsPaused() ? resume_bitmap : pause_bitmap);
if (show_labels) pause_resume_btn->SetLabel(playback->IsPaused() ? "Resume" : "Pause");
} else {
position_label->SetText("Stopped.");
slider->Hide();
position_label->Show();
if (show_icons) pause_resume_btn->SetIcon(pause_bitmap);
if (show_labels) pause_resume_btn->SetLabel("Pause");
}
if (!show_icons) pause_resume_btn->SetIcon(get_empty_icon());
if (!show_labels) pause_resume_btn->SetLabel("");
slider->SetEnabled(enable_ui);
pause_resume_btn->SetEnabled(enable_ui);
auto volume = playback->GetVolume();
auto pitch = playback->GetPitch();
auto speed = playback->GetSpeed();
auto tempo = playback->GetTempo();
if (!volume_clicked) volume_slider->SetValue(volume);
volume_slider->SetLabel(fmt::format("Volume: {}%", (int)volume).c_str());
if (!pitch_clicked) pitch_slider->SetValueDouble(pitch);
pitch_slider->SetLabel(fmt::format("Pitch {:.02f}x", pitch).c_str());
if (!speed_clicked) speed_slider->SetValueDouble(speed);
speed_slider->SetLabel(fmt::format("Speed: {:.02f}x", speed).c_str());
if (!tempo_clicked) tempo_slider->SetValueDouble(tempo);
tempo_slider->SetLabel(fmt::format("Tempo: {:.02f}x", tempo).c_str());
UpdateIfNeeded();
auto title = playback->get_current_title();
if (title.has_value()) {
SetTitle(fmt::format("{} - Looper", title.value()).c_str());
} else {
SetTitle("Looper");
}
}
void HaikuLooperWindow::FrameResized(float newWidth, float newHeight) {
InvalidateLayout(true);
Layout(true);
layout->SetExplicitSize(BSize(newWidth, newHeight));
}
void HaikuLooperWindow::ThreadFunc() {
while (true) {
if (done) return;
if (Lock()) {
if (!IsHidden()) {
Unlock();
break;
}
Unlock();
} else {
break;
}
}
while (true) {
if (done) return;
if (Lock()) {
if (IsHidden()) {
Unlock();
break;
}
PostMessage(CMD_UPDATE);
Unlock();
} else {
break;
}
std::this_thread::sleep_for(1s / 60.0);
}
}
HaikuLooperRefHandler::HaikuLooperRefHandler(HaikuLooperWindow *win) {
this->win = win;
be_app->Lock();
this->next_handler = be_app->PreferredHandler();
be_app->AddHandler(this);
be_app->SetPreferredHandler(this);
if (next_handler != NULL) {
SetNextHandler(next_handler);
}
AddFilter(new BMessageFilter(B_REFS_RECEIVED));
be_app->Unlock();
}
void HaikuLooperRefHandler::MessageReceived(BMessage *msg) {
if (msg->what == B_REFS_RECEIVED) {
win->PostMessage(msg);
}
}