From c1665b33e2e0c216d7a287ae41c62195e5037269 Mon Sep 17 00:00:00 2001 From: Hane Date: Mon, 13 May 2024 16:27:35 +0200 Subject: [PATCH 01/38] removed SVV.exe --- assets/SoundVolumeView.exe | Bin 203592 -> 0 bytes bueno.bat | 2 - src/back/backlasses.cpp | 90 +++++++++---------------------------- src/back/backlasses.h | 11 +++-- 4 files changed, 27 insertions(+), 76 deletions(-) delete mode 100644 assets/SoundVolumeView.exe diff --git a/assets/SoundVolumeView.exe b/assets/SoundVolumeView.exe deleted file mode 100644 index 4c7a7d2fe0e11b39fec51ffb8152ecd41e19e9c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 203592 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

~>seLx`ep|j5*LqYWqW(|Ths?p)KLc^ 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 XHi6$MX1&XX;<+8{CO*bT;E4YNbQI2I diff --git a/bueno.bat b/bueno.bat index edb534d..6aa7e4e 100644 --- a/bueno.bat +++ b/bueno.bat @@ -1,5 +1,3 @@ taskkill /F /IM "qtest.exe" qmake -o build\Makefile .\qtest.pro -copy /Y /B .\assets\SoundVolumeView.exe .\build\debug -copy /Y /B .\assets\SoundVolumeView.exe .\build\release mingw32-make.exe -C .\build -f Makefile diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index e398b21..e6e691d 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -228,10 +228,10 @@ HRESULT EndpointSituationCallback::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, return S_OK; } -Endpoint::Endpoint(IMMDevice* ep, uint64_t idx){ +Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ this->endpoint = ep; this->idx = idx; - + this->policyConfig = policyConfig; /* * It can't multiflag, it's that stupid. MS momento. * Only shows most relevant flag according to MS, i.e. 0110 sends 0010 @@ -434,70 +434,30 @@ Roles Endpoint::getRoles(){ } void Endpoint::setRoles(Roles role){ - //todo: otro exe momento - STARTUPINFOEXW startupConfig; - PROCESS_INFORMATION processInfo; - SecureZeroMemory(&startupConfig, sizeof(STARTUPINFOEXW)); - SecureZeroMemory(&startupConfig.StartupInfo, sizeof(STARTUPINFOW)); - startupConfig.StartupInfo.cb = sizeof(STARTUPINFOEXW); - SecureZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION)); + if (!policyConfig) return; - std::wstring command = L"SoundVolumeView.exe /SetDefault " + endpointId + L" "; - std::wstring troublePair = L"1"; - switch (role) { - case Roles::ROLE_ALL: - /* - * console or multimedia, one sends both, at least for now; - * either cos of ms or dis guy; - * no choice but to treat them as one for now. - * command += L"all"; and nothing else would've been nice... - */ - troublePair = command + troublePair; - if(CreateProcessW( - NULL, - (wchar_t*)troublePair.c_str(), - NULL, - NULL, - false, - CREATE_UNICODE_ENVIRONMENT, - NULL, - NULL, - (LPSTARTUPINFOW)&startupConfig, - &processInfo - ) == true) { - WaitForSingleObject(processInfo.hProcess, INFINITE ); - CloseHandle(processInfo.hProcess); - CloseHandle(processInfo.hThread); - } - command += L"2"; - break; + bool allRoles = false; + ERole val; + switch(role) { case Roles::ROLE_CONSOLE: - command += std::to_wstring(0); + val = eConsole; break; case Roles::ROLE_MULTIMEDIA: - command += std::to_wstring(1); + val = eMultimedia; break; case Roles::ROLE_COMMUNICATIONS: - command += std::to_wstring(2); + val = eCommunications; + break; + default: + allRoles = true; break; } - - if(CreateProcessW( - NULL, - (wchar_t*)command.c_str(), - NULL, - NULL, - false, - CREATE_UNICODE_ENVIRONMENT, - NULL, - NULL, - (LPSTARTUPINFOW)&startupConfig, - &processInfo - ) == true) { - WaitForSingleObject(processInfo.hProcess, INFINITE ); - CloseHandle(processInfo.hProcess); - CloseHandle(processInfo.hThread); - } + if (allRoles) { + policyConfig->SetDefaultEndpoint(endpointId.c_str(), eMultimedia); + //policyConfig->SetDefaultEndpoint(endpointId.c_str(), eConsole); + policyConfig->SetDefaultEndpoint(endpointId.c_str(), eCommunications); + } + policyConfig->SetDefaultEndpoint(endpointId.c_str(), val); } void Endpoint::assignRoles(Roles role){ @@ -599,7 +559,7 @@ void Overseer::reloadEndpoints(Flows flow) { IMMDevice *temp; for (unsigned int i = 0; i < numEndpoints; i++){ if(deviceCollection->Item(i, &temp) != 0) { log_debugcpp("si"); }; - Endpoint *endpoint = new Endpoint(temp, i); + Endpoint *endpoint = new Endpoint(temp, policyConfig, i); if (flow == Flows::FLOW_PLAYBACK) this->playbackDevices.push_back(endpoint); else @@ -608,16 +568,6 @@ void Overseer::reloadEndpoints(Flows flow) { } deviceCollection->Release(); - //IPolicyConfig7 test - /* - * deviceEnumerator->GetDefaultAudioEndpoint(MSflow, ERole::eCommunications, &temp); - * LPWSTR tempString = nullptr; - * temp->GetId(&tempString); - * HRESULT hre = policyConfig->SetDefaultEndpoint( - * tempString, - * ERole::eMultimedia - * ); - */ /* * Discerning default endpoints per role @@ -670,7 +620,7 @@ Endpoint* Overseer::addEndpoint(std::wstring endpointId, /* out */Flows* flow = if(FAILED(deviceEnumerator->GetDevice((LPCWSTR)endpointId.c_str(), &newep))) log_debugcpp("ay caramba con la hot metida."); - Endpoint *endpoint = new Endpoint(newep); + Endpoint *endpoint = new Endpoint(newep, policyConfig); Flows getFlow = endpoint->getFlow(); if (getFlow == Flows::FLOW_PLAYBACK) { diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 0cee06e..bcc3306 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -11,7 +11,7 @@ class Session; class Endpoint { public: - Endpoint(IMMDevice* endpoint, uint64_t idx = 0); + Endpoint(IMMDevice* endpoint, IPolicyConfig7* policyConfig, uint64_t idx = 0); //todo: how to forward declare delegate constructors? //Endpoint(IMMDevice* endpoint) : Endpoint(endpoint, 0) {}; void reloadEndpointChannels(); @@ -69,7 +69,7 @@ class Endpoint { uint64_t idx; //Not implemented in llvm-mingw. Sad! todo: mingw patch IAudioMeterInformation *endpointPeakMeter = nullptr; - + IPolicyConfig7* policyConfig; }; class EndpointVolumeCallback : public IAudioEndpointVolumeCallback { @@ -128,15 +128,18 @@ class Overseer { ~Overseer(); private: + void initCOMLibrary(); + NGuid guid; IMMDeviceEnumerator *deviceEnumerator; EndpointSituationCallback epsc; - //IPolicyConfig *policyConfig; + std::vector playbackDevices; std::vector captureDevices; - void initCOMLibrary(); + IPolicyConfig7* policyConfig; + friend class Endpoint; //IMMDeviceCollection *deviceCollection; //int numCaptureEndpoints; //std::vector *captureDevices; From 5d179bb91c3f4ecdc66eb69ca87feeccef068ba3 Mon Sep 17 00:00:00 2001 From: Hane Date: Mon, 13 May 2024 17:00:54 +0200 Subject: [PATCH 02/38] changed qmake recipe (static c++/unwind link) --- qtest.pro | 2 +- src/back/backlasses.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/qtest.pro b/qtest.pro index 4677fd8..42b246a 100644 --- a/qtest.pro +++ b/qtest.pro @@ -1,6 +1,6 @@ QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview QMAKE_LFLAGS += --target=x86_64-w64-mingw32 -g -Wl,-pdb= -v -LIBS += -LC:/capybara/libclang/x86_64-w64-mingw32/lib -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys +LIBS += -LC:/capybara/libclang/x86_64-w64-mingw32/lib -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys -static -stdlib=libc++ -lunwind #"kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 DEFINES += DEBUG QT_LOGGING_TO_CONSOLE=1 WIN32_LEAN_AND_MEAN CONFIG += debug diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index e6e691d..1f7b442 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -456,8 +456,7 @@ void Endpoint::setRoles(Roles role){ policyConfig->SetDefaultEndpoint(endpointId.c_str(), eMultimedia); //policyConfig->SetDefaultEndpoint(endpointId.c_str(), eConsole); policyConfig->SetDefaultEndpoint(endpointId.c_str(), eCommunications); - } - policyConfig->SetDefaultEndpoint(endpointId.c_str(), val); + } else policyConfig->SetDefaultEndpoint(endpointId.c_str(), val); } void Endpoint::assignRoles(Roles role){ From b6b7e1c577b379e2435e34d5ec9b743b14b39962 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 14 May 2024 19:48:11 +0200 Subject: [PATCH 03/38] code cleanup: refactored ugly window width/scrheight cache --- src/qt/qtclasses.cpp | 13 ++++++++++--- src/qt/qtclasses.h | 4 +--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 5e0bd3a..efac234 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -633,7 +633,16 @@ void EndpointWidget::addSessionWidget(CustomWidgetEvent* ev){ uint64_t index = this->sessionWidgets.size(); SessionWidget* sw = new SessionWidget(index, ev->payload, this); ev->payload->setFrontIndex(index); - sw->calculateSize(currentWidth, currentHeight); + //MainWindow* mw = dynamic_cast(parent()); + //TODO: change mainwindow's widget name and subclass qwidget + const QWidgetList topLevelWidgets = QApplication::topLevelWidgets(); + for (QWidget *widget : topLevelWidgets) { + if (dynamic_cast(widget)) { + double widthRatio = ((MainWindow*)widget)->widthRatio; + sw->calculateSize(std::abs(this->screen()->geometry().width()) * widthRatio, + std::abs(this->screen()->geometry().height())); + } + } this->widgetLayout->addWidget(sw, row, 0, 1, 4); row++; sessionWidgets.push_back(sw); @@ -740,8 +749,6 @@ void MainWindow::reorderEndpointWidgetCollection() { void EndpointWidget::calculateSize(uint64_t width, uint64_t height) { /* og 1080p 120% testing values */ - this->currentWidth = width; - this->currentHeight = height; log_to_file("[EndpointWidget %s sizes]\n", converter.to_bytes(this->getEndpointHandler()->getName()).c_str()); log_to_file("Params: {Width: %u Height: %u}\n", width, height); this->mainLabel->setMaximumWidth((int)(width * 0.50) /* 1080p 120%*/); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 854e7d8..c85b9ad 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -201,8 +201,6 @@ private: ChannelWidget* cw; std::vector sessionWidgets; QSize minimum; - uint64_t currentWidth = 1; - uint64_t currentHeight = 1; //std::vector *ephs; //std::vector *sliders; @@ -272,7 +270,7 @@ private: QToolBar *mainMenuBar; QScreen *screen; QSpacerItem* lastRowSpacer; - + friend class EndpointWidget; //public slots: // void setEndpointHandlers(std::vector *ephs); From 0e123f886dc8874c1e0988620fe9d496f26e44b3 Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 16 May 2024 17:23:54 +0200 Subject: [PATCH 04/38] sliders value set to where clicked --- qtest.pro | 2 +- src/back/backlasses.cpp | 4 +- src/qt/qtclasses.cpp | 25 +++++++++--- src/qt/qtclasses.h | 11 +++--- src/qt/qtvisuals.h | 84 +++++++++++++++++++++++++++++++++++++++++ src/qtestmain.cpp | 4 +- 6 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 src/qt/qtvisuals.h diff --git a/qtest.pro b/qtest.pro index 42b246a..97274fa 100644 --- a/qtest.pro +++ b/qtest.pro @@ -11,7 +11,7 @@ DESTPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" SOURCES += qtestmain.cpp qtclasses.cpp backlasses.cpp backsessionclasses.cpp contclasses.cpp contsessionclasses.cpp -HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h +HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h qtvisuals.h RESOURCES = assets.qrc RC_ICONS += assets/logo.ico diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 1f7b442..bfc0e83 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -1,5 +1,5 @@ -#include -#include +#include "backlasses.h" +#include "backfuncs.h" EndpointNewSessionCallback::EndpointNewSessionCallback(EndpointHandler* eph){ this->eph = eph; diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index efac234..2174d7d 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -6,6 +6,19 @@ CustomWidgetEvent::CustomWidgetEvent(QEvent::Type type, T payload) : QEvent(t this->payload = payload; } + +/* + * MeterSlider::MeterSlider(Qt::Orientation orientation, QWidget* parent) { + * //style = new MixerStyle(); + * //this->setStyle(style); + * } + */ + + +MeterSlider::~MeterSlider() { + //delete style; +} + void MeterSlider::setPeakValue(float peakValue) { this->peakValue = peakValue; } @@ -99,7 +112,7 @@ void MeterSlider::paintEvent(QPaintEvent *event) { ((QWidget*)parent())->layout()->getContentsMargins(&left, &top, &right, &bottom); QStyle *style = QApplication::style(); - int lol = style->pixelMetric(QStyle::PM_SliderSpaceAvailable); + //int lol = style->pixelMetric(QStyle::PM_SliderSpaceAvailable); QPainter painter(this); //painter.setPen(Qt::blue); painter.setOpacity(1.0); @@ -125,8 +138,7 @@ void MeterSlider::paintEvent(QPaintEvent *event) { // - ((this->maximum() - this->value()) * stepWidth)) //double ratio = ; double handleShift = (double)((sliderSize.width() * ((double)(this->maximum() - this->value()) / 100))); - painter.fillRect((this->width() - ((this->maximum() - this->value()) * stepWidth)) - (sliderSize.width()) + handleShift, - top / 2, sliderSize.width(), sliderSize.height() - bottom, Qt::magenta); + painter.fillRect((this->width() - ((this->maximum() - this->value()) * stepWidth)) - (sliderSize.width()) + handleShift, top / 2, sliderSize.width(), sliderSize.height() - bottom, Qt::magenta); //sliderComplex.subControls = QStyle::SC_SliderHandle; //p.drawComplexControl(QStyle::CC_Slider, sliderComplex); } @@ -637,7 +649,7 @@ void EndpointWidget::addSessionWidget(CustomWidgetEvent* ev){ //TODO: change mainwindow's widget name and subclass qwidget const QWidgetList topLevelWidgets = QApplication::topLevelWidgets(); for (QWidget *widget : topLevelWidgets) { - if (dynamic_cast(widget)) { + if (qobject_cast(widget)) { double widthRatio = ((MainWindow*)widget)->widthRatio; sw->calculateSize(std::abs(this->screen()->geometry().width()) * widthRatio, std::abs(this->screen()->geometry().height())); @@ -917,8 +929,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); //scrollArea->verticalScrollBar()->setSingleStep(1); - - scrollArea->setStyleSheet("QScrollBar:vertical { width: 4px; }"); + + //custom style = no qss + //scrollArea->setStyleSheet("QScrollBar:vertical { width: 4px; }"); //scrollArea->setMinimumWidth(500); setCentralWidget(scrollArea); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index c85b9ad..fd9f936 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -1,8 +1,5 @@ #pragma once -//#ifndef MAINWINDOW_H -//#define MAINWINDOW_H - #include #include #include @@ -47,6 +44,7 @@ #include "global.h" #include "contclasses.h" +#include "qtvisuals.h" enum SpawnPos { LEFT = (1 << 1), @@ -76,12 +74,17 @@ public: class MeterSlider : public QSlider { Q_OBJECT private: + ~MeterSlider(); float peakValue; + MixerStyle* style; + protected: bool event(QEvent* ev) override; void paintEvent(QPaintEvent *event) override; public: + //MeterSlider(Qt::Orientation orientation, QWidget *parent = nullptr); + //MeterSlider(QWidget* parent = nullptr) : MeterSlider(Qt::Vertical, parent){}; void setPeakValue(float peakValue); using QSlider::QSlider; }; @@ -278,5 +281,3 @@ private: //void valueChanged(int value); }; - -//#endif diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h new file mode 100644 index 0000000..5aba5ca --- /dev/null +++ b/src/qt/qtvisuals.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +class MixerStyle : public QProxyStyle { + Q_OBJECT + +public: + using QProxyStyle::QProxyStyle; + + //void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, + //QPainter *p, const QWidget *widget) const override; + + int styleHint(QStyle::StyleHint hint, const QStyleOption* option = 0, const QWidget* widget = 0, + QStyleHintReturn* returnData = 0) const { + if (hint == QStyle::SH_Slider_AbsoluteSetButtons) + return (Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); + return QProxyStyle::styleHint(hint, option, widget, returnData); + } +}; + + +/* + * void MixerStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, + * QPainter *p, const QWidget *widget) const { + * #if QT_CONFIG(slider) + * case CC_Slider: + * if (const QStyleOptionSlider *slider = qstyleoption_cast(opt)) { + * if (slider->subControls == SC_SliderTickmarks) { + * int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget); + * int ticks = slider->tickPosition; + * int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget); + * int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + * int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + * int interval = slider->tickInterval; + * if (interval <= 0) { + * interval = slider->singleStep; + * if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, + * available) + * - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + * 0, available) < 3) + * interval = slider->pageStep; + * } + * if (!interval) + * interval = 1; + * int fudge = len / 2; + * int pos; + * // Since there is no subrect for tickmarks do a translation here. + * QPainterStateSaver pss(p); + * p->translate(slider->rect.x(), slider->rect.y()); + * p->setPen(slider->palette.windowText().color()); + * int v = slider->minimum; + * while (v <= slider->maximum + 1) { + * if (v == slider->maximum + 1 && interval == 1) + * break; + * const int v_ = qMin(v, slider->maximum); + * pos = QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + * v_, available) + fudge; + ** + * if (slider->orientation == Qt::Horizontal) { + * if (ticks & QSlider::TicksAbove) + * p->drawLine(pos, 0, pos, tickOffset - 2); + * if (ticks & QSlider::TicksBelow) + * p->drawLine(pos, tickOffset + thickness + 1, pos, + * slider->rect.height()-1); + * } else { + * if (ticks & QSlider::TicksAbove) + * p->drawLine(0, pos, tickOffset - 2, pos); + * if (ticks & QSlider::TicksBelow) + * p->drawLine(tickOffset + thickness + 1, pos, + * slider->rect.width()-1, pos); + * } + * // in the case where maximum is max int + * int nextInterval = v + interval; + * if (nextInterval < v) + * break; + * v = nextInterval; + * } + * } + * } + * break; +*#endif // QT_CONFIG(slider) +*/ +//} diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index 1d421cc..e4a6aec 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -46,13 +46,15 @@ void closeDebugFileLog() { */ int main (int argc, char* argv[]) { + /* * QStringList styles = QStyleFactory::keys(); * for(QString a : styles) { * log_debugcpp(a.toStdString()); * } */ - //QApplication::setStyle("Fusion"); + + QApplication::setStyle(new MixerStyle(QStyleFactory::create("windowsvista"))); //Check if running //https://stackoverflow.com/questions/48060989/qt-show-application-if-currently-running initialize_file_log(); From f8171f12f310938b1de65f27d67d92c41358754f Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 21 May 2024 19:35:12 +0200 Subject: [PATCH 05/38] wip: bad build config, unnecesary reimpl. Step by step --- qtest.pro | 2 +- src/qt/qtclasses.cpp | 145 +++++----- src/qt/qtclasses.h | 8 +- src/qt/qtvisuals.h | 613 ++++++++++++++++++++++++++++++++++++++----- src/qtestmain.cpp | 7 +- 5 files changed, 640 insertions(+), 135 deletions(-) diff --git a/qtest.pro b/qtest.pro index 97274fa..63ef85f 100644 --- a/qtest.pro +++ b/qtest.pro @@ -11,7 +11,7 @@ DESTPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" SOURCES += qtestmain.cpp qtclasses.cpp backlasses.cpp backsessionclasses.cpp contclasses.cpp contsessionclasses.cpp -HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h qtvisuals.h +HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h RESOURCES = assets.qrc RC_ICONS += assets/logo.ico diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 2174d7d..6e90375 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -34,6 +34,7 @@ bool MeterSlider::event(QEvent* ev) { void MeterSlider::paintEvent(QPaintEvent *event) { QStyleOptionSlider sliderComplex = QStyleOptionSlider(); sliderComplex.initFrom(this); + /* * sliderComplex.initFrom(this); * sliderComplex.subControls = QStyle::SC_None; @@ -64,36 +65,46 @@ void MeterSlider::paintEvent(QPaintEvent *event) { * //sliderComplex.subControls = QStyle::SC_SliderGroove; * if (this->tickPosition() != NoTicks) sliderComplex.subControls |= QStyle::SC_SliderTickmarks; * QStylePainter p(this); - * p.drawComplexControl(QStyle::CC_Slider, sliderComplex); */ + //p.drawComplexControl(QStyle::CC_Slider, sliderComplex); + - /* - * QStyleOptionSlider sliderComplex2 = QStyleOptionSlider(); - * sliderComplex2.initFrom(this); - * sliderComplex2.orientation = this->orientation(); - * sliderComplex2.maximum = this->maximum(); - * sliderComplex2.minimum = this->minimum(); - * sliderComplex2.tickPosition = (QSlider::TickPosition)this->tickPosition(); - * sliderComplex2.tickInterval = this->tickInterval(); - * sliderComplex2.upsideDown = (this->orientation() == Qt::Horizontal) ? - * (this->invertedAppearance() != (sliderComplex2.direction == Qt::RightToLeft)) - * : (!this->invertedAppearance()); - * sliderComplex2.subControls = QStyle::SC_SliderHandle; - * sliderComplex2.direction = Qt::LeftToRight; // we use the upsideDown option instead - * sliderComplex2.sliderPosition = this->sliderPosition(); - * sliderComplex2.sliderValue = this->value(); - * sliderComplex2.singleStep = this->singleStep(); - * sliderComplex2.pageStep = this->pageStep(); - * if (this->orientation() == Qt::Horizontal) - * sliderComplex2.state |= QStyle::State_Horizontal; - * - * if (this->isSliderDown()) { - * sliderComplex2.activeSubControls = QStyle::SC_SliderHandle; - * sliderComplex2.state |= QStyle::State_Sunken; - * } else { - * sliderComplex2.activeSubControls = QStyle::SC_SliderHandle; - * } - */ + + QStyleOptionSlider sliderComplex2 = QStyleOptionSlider(); + sliderComplex2.initFrom(this); + sliderComplex2.orientation = this->orientation(); + sliderComplex2.maximum = this->maximum(); + sliderComplex2.minimum = this->minimum(); + sliderComplex2.tickPosition = (QSlider::TickPosition)this->tickPosition(); + sliderComplex2.tickInterval = this->tickInterval(); + sliderComplex2.upsideDown = (this->orientation() == Qt::Horizontal) ? + (this->invertedAppearance() != (sliderComplex2.direction == Qt::RightToLeft)) + : (!this->invertedAppearance()); + sliderComplex2.subControls = QStyle::SC_SliderHandle; + sliderComplex2.direction = Qt::LeftToRight; // we use the upsideDown option instead + sliderComplex2.sliderPosition = this->sliderPosition(); + sliderComplex2.sliderValue = this->value(); + sliderComplex2.singleStep = this->singleStep(); + sliderComplex2.pageStep = this->pageStep(); + if (this->orientation() == Qt::Horizontal) + sliderComplex2.state |= QStyle::State_Horizontal; + + if (this->isSliderDown()) { + sliderComplex2.activeSubControls = QStyle::SC_SliderHandle; + sliderComplex2.state |= QStyle::State_Sunken; + } else { + sliderComplex2.activeSubControls = QStyle::SC_SliderHandle; + } + + sliderComplex2.subControls = QStyle::SC_SliderGroove | QStyle::SC_SliderHandle; + if ((QSlider::TickPosition)this->tickPosition() != NoTicks) + sliderComplex2.subControls |= QStyle::SC_SliderTickmarks; + + QPainter painter(this); + QStyle* stle = QApplication::style(); + stle->drawComplexControl((QStyle::ComplexControl)CC_MeterSlider, &sliderComplex2, &painter, this); + + //Q_D(QSlider); /* @@ -108,39 +119,43 @@ void MeterSlider::paintEvent(QPaintEvent *event) { * //p.drawComplexControl(QStyle::CC_Slider, opt); */ //QSlider::paintEvent(event); - int left = 0, top = 0, right = 0, bottom = 0; - ((QWidget*)parent())->layout()->getContentsMargins(&left, &top, &right, &bottom); - QStyle *style = QApplication::style(); - //int lol = style->pixelMetric(QStyle::PM_SliderSpaceAvailable); - QPainter painter(this); - //painter.setPen(Qt::blue); - painter.setOpacity(1.0); - painter.setClipping(false); - painter.setCompositionMode(QPainter::CompositionMode::CompositionMode_Source); - float peakLength = (this->width() * (this->peakValue)); - double stepWidth = (double)this->width() * ((double)this->singleStep() / (this->maximum() - this->minimum())); - //Fusion seems to fuck around with bar's height and width - //const qreal dpr = painter->device()->devicePixelRatio(); - //QStyleOptionSlider sliderComplex = QStyleOptionSlider(); //slider.initFrom(this); - QRect sliderSize = style->subControlRect(QStyle::CC_Slider, (QStyleOptionComplex*)&sliderComplex, QStyle::SC_SliderHandle); - int handleCenterPos = QStyle::sliderPositionFromValue(this->minimum(), this->maximum(), this->value(), this->width()); - int unattenuatedPeakMeter = ((this->width() * this->peakValue) >= handleCenterPos - (sliderSize.width() / 2)) - ? this->width() - (sliderSize.width() / 2) - : this->width() * this->peakValue; - //QApplication::style()->subControlRect(QStyle::CC_Slider, (QStyleOptionComplex*)&slider, QStyle::SC_SliderHandle); - painter.fillRect(0, (this->height() / 2) - 3, this->width(), - 4, Qt::white); - painter.fillRect(0, (this->height() / 2) - 3, this->width() * this->peakValue, - 4, Qt::gray); - painter.fillRect(0, (this->height() / 2) - 3, (this->width() - ((this->maximum() - this->value()) * stepWidth)) * this->peakValue, - 4, Qt::green); - // - ((this->maximum() - this->value()) * stepWidth)) - //double ratio = ; - double handleShift = (double)((sliderSize.width() * ((double)(this->maximum() - this->value()) / 100))); - painter.fillRect((this->width() - ((this->maximum() - this->value()) * stepWidth)) - (sliderSize.width()) + handleShift, top / 2, sliderSize.width(), sliderSize.height() - bottom, Qt::magenta); - //sliderComplex.subControls = QStyle::SC_SliderHandle; - //p.drawComplexControl(QStyle::CC_Slider, sliderComplex); + //IL CODE WENO lbockomet + /* + * int left = 0, top = 0, right = 0, bottom = 0; + * ((QWidget*)parent())->layout()->getContentsMargins(&left, &top, &right, &bottom); + * + * QStyle *style = QApplication::style(); + * //int lol = style->pixelMetric(QStyle::PM_SliderSpaceAvailable); + * QPainter painter(this); + * //painter.setPen(Qt::blue); + * painter.setOpacity(1.0); + * painter.setClipping(false); + * painter.setCompositionMode(QPainter::CompositionMode::CompositionMode_Source); + * float peakLength = (this->width() * (this->peakValue)); + * double stepWidth = (double)this->width() * ((double)this->singleStep() / (this->maximum() - this->minimum())); + * //Fusion seems to fuck around with bar's height and width + * //const qreal dpr = painter->device()->devicePixelRatio(); + * //QStyleOptionSlider sliderComplex = QStyleOptionSlider(); //slider.initFrom(this); + * QRect sliderSize = style->subControlRect(QStyle::CC_Slider, (QStyleOptionComplex*)&sliderComplex, QStyle::SC_SliderHandle); + * int handleCenterPos = QStyle::sliderPositionFromValue(this->minimum(), this->maximum(), this->value(), this->width()); + * int unattenuatedPeakMeter = ((this->width() * this->peakValue) >= handleCenterPos - (sliderSize.width() / 2)) + * ? this->width() - (sliderSize.width() / 2) + * : this->width() * this->peakValue; + * //QApplication::style()->subControlRect(QStyle::CC_Slider, (QStyleOptionComplex*)&slider, QStyle::SC_SliderHandle); + * painter.fillRect(0, (this->height() / 2) - 3, this->width(), + * 4, Qt::white); + * painter.fillRect(0, (this->height() / 2) - 3, this->width() * this->peakValue, + * 4, Qt::gray); + * painter.fillRect(0, (this->height() / 2) - 3, (this->width() - ((this->maximum() - this->value()) * stepWidth)) * this->peakValue, + * 4, Qt::green); + * // - ((this->maximum() - this->value()) * stepWidth)) + * //double ratio = ; + * double handleShift = (double)((sliderSize.width() * ((double)(this->maximum() - this->value()) / 100))); + * painter.fillRect((this->width() - ((this->maximum() - this->value()) * stepWidth)) - (sliderSize.width()) + handleShift, top / 2, sliderSize.width(), sliderSize.height() - bottom, Qt::magenta); + * //sliderComplex.subControls = QStyle::SC_SliderHandle; + * //p.drawComplexControl(QStyle::CC_Slider, sliderComplex); + */ } void ExtendedCheckBox::customEvent(QEvent* ev) { @@ -317,8 +332,8 @@ SessionWidget::SessionWidget(uint64_t idx, SessionHandler* sh, QWidget *parent) mainSlider->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); //mainSlider->setMinimumWidth(120 /*1/16 1080p*/); mainSlider->setFocusPolicy(Qt::StrongFocus); - mainSlider->setTickPosition(QSlider::TicksBothSides); - mainSlider->setTickInterval(5); + //mainSlider->setTickPosition(QSlider::TicksBothSides); + //mainSlider->setTickInterval(5); mainSlider->setSingleStep(1); mainSlider->setRange(0,100); @@ -428,7 +443,7 @@ ChannelWidget::ChannelWidget(uint32_t channelCount, EndpointHandler* eph, QWidge for(uint64_t channel = 0, col = 0, row = 0; channel < channelCount && channelCount > 1; channel++){ MeterSlider* tmp = new MeterSlider(Qt::Horizontal); QLabel* tmpLb = new QLabel(""); - tmp->setTickInterval(5); + //tmp->setTickInterval(5); tmp->setSingleStep(1); tmp->setRange(0,100); @@ -520,8 +535,8 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i //muteButton->setStyleSheet("background-color: #A3C1DA; color: red"); mainSlider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); mainSlider->setFocusPolicy(Qt::StrongFocus); - mainSlider->setTickPosition(QSlider::TicksBothSides); - mainSlider->setTickInterval(5); + //mainSlider->setTickPosition(QSlider::TicksBothSides); + //mainSlider->setTickInterval(5); mainSlider->setSingleStep(1); mainSlider->setRange(0,100); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index fd9f936..47216be 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -29,7 +29,7 @@ #include #include #include - +#include //#include /* * #else @@ -44,7 +44,6 @@ #include "global.h" #include "contclasses.h" -#include "qtvisuals.h" enum SpawnPos { LEFT = (1 << 1), @@ -76,8 +75,8 @@ class MeterSlider : public QSlider { private: ~MeterSlider(); float peakValue; - MixerStyle* style; + friend class MixerStyle; protected: bool event(QEvent* ev) override; void paintEvent(QPaintEvent *event) override; @@ -89,6 +88,9 @@ public: using QSlider::QSlider; }; +//todo: TEST. TEST. +#include "qtvisuals.h" + class ExtendedCheckBox : public QCheckBox { Q_OBJECT protected: diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h index 5aba5ca..bad98bb 100644 --- a/src/qt/qtvisuals.h +++ b/src/qt/qtvisuals.h @@ -1,84 +1,567 @@ #pragma once #include +#include +#include +#include +//#include "qstylehelper.cpp" + +//repeats. bruh. +//#include +//#include + +enum CustomComplexControl { + CC_MeterSlider = 0xf0000001 +}; + + +namespace qt64 { + static inline QLatin1String operator""_L1(const char* ch, uint64_t) { + return QLatin1String(ch); + } +} + +using namespace qt64; + class MixerStyle : public QProxyStyle { - Q_OBJECT - public: using QProxyStyle::QProxyStyle; - //void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, - //QPainter *p, const QWidget *widget) const override; - + //void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, + // QPainter *painter, const QWidget *widget) const override; + //Click on slider = move handle to click pos int styleHint(QStyle::StyleHint hint, const QStyleOption* option = 0, const QWidget* widget = 0, QStyleHintReturn* returnData = 0) const { if (hint == QStyle::SH_Slider_AbsoluteSetButtons) return (Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); return QProxyStyle::styleHint(hint, option, widget, returnData); } + + /* + * #ifdef Q_OS_DARWIN + * static const qreal qstyleBaseDpi = 72; + * #else + * static const qreal qstyleBaseDpi = 96; + * #endif + */ + + //Q_GUI_EXPORT int qt_defaultDpiX() const; + + qreal dpi(const QStyleOption *option) const { + #ifndef Q_OS_DARWIN + // Prioritize the application override, except for on macOS where + // we have historically not supported the AA_Use96Dpi flag. + if (QCoreApplication::testAttribute(Qt::AA_Use96Dpi)) + return 96; + #endif + + // Expect that QStyleOption::QFontMetrics::QFont has the correct DPI set + if (option) + return option->fontMetrics.fontDpi(); + + // Fall back to historical Qt behavior: hardocded 72 DPI on mac, + // primary screen DPI on other platforms. + #ifdef Q_OS_DARWIN + return qstyleBaseDpi; + #else + return QGuiApplication::primaryScreen()->physicalDotsPerInch(); + #endif + } + + qreal dpiScaled(qreal value, qreal dpi) const { + static const qreal qstyleBaseDpi = 96; + return value * dpi / qstyleBaseDpi; + } + + qreal dpiScaled(qreal value, const QPaintDevice *device) const { + return dpiScaled(value, device->logicalDpiX()); + } + + qreal dpiScaled(qreal value, const QStyleOption *option) const { + return dpiScaled(value, dpi(option)); + } + + QRect subControlRect(ComplexControl control, const QStyleOptionComplex *option, + SubControl subControl, const QWidget *widget) const { + QRect rect = QCommonStyle::subControlRect(CC_Slider, option, subControl, widget); + + switch (control) { + #if QT_CONFIG(slider) + case CC_MeterSlider: + if (const QStyleOptionSlider *slider = qstyleoption_cast(option)) { + int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); + switch (subControl) { + case SC_SliderHandle: { + const bool bothTicks = (slider->tickPosition & QSlider::TicksBothSides) == QSlider::TicksBothSides; + if (slider->orientation == Qt::Horizontal) { + rect.setHeight(baseStyle()->pixelMetric(PM_SliderThickness, option, widget)); + rect.setWidth(baseStyle()->pixelMetric(PM_SliderLength, option, widget)); + int centerY = slider->rect.center().y() - rect.height() / 2; + if (!bothTicks) { + if (slider->tickPosition & QSlider::TicksAbove) + centerY += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + centerY -= tickSize - 1; + } + rect.moveTop(centerY); + } else { + rect.setWidth(baseStyle()->pixelMetric(PM_SliderThickness, option, widget)); + rect.setHeight(baseStyle()->pixelMetric(PM_SliderLength, option, widget)); + int centerX = slider->rect.center().x() - rect.width() / 2; + if (!bothTicks) { + if (slider->tickPosition & QSlider::TicksAbove) + centerX += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + centerX -= tickSize - 1; + } + rect.moveLeft(centerX); + } + } + break; + case SC_SliderGroove: { + QPoint grooveCenter = slider->rect.center(); + //rect.setWidth(std::abs(grooveCenter.x()) * 2); + const int grooveThickness = this->dpiScaled(7, option); //QStyleHelper::dpiScaled(7, option); + const bool bothTicks = (slider->tickPosition & QSlider::TicksBothSides) == QSlider::TicksBothSides; + if (slider->orientation == Qt::Horizontal) { + rect.setHeight(grooveThickness); + if (!bothTicks) { + if (slider->tickPosition & QSlider::TicksAbove) + grooveCenter.ry() += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + grooveCenter.ry() -= tickSize - 1; + } + } else { + rect.setWidth(grooveThickness); + if (!bothTicks) { + if (slider->tickPosition & QSlider::TicksAbove) + grooveCenter.rx() += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + grooveCenter.rx() -= tickSize - 1; + } + } + rect.moveCenter(grooveCenter); + break; + } + default: + break; + } + } + return rect; + break; +#endif // QT_CONFIG(slider) + default: + return baseStyle()->subControlRect(control, option, subControl, widget); + break; + } + + } + + void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, + QPainter *painter, const QWidget *widget) const { + switch (cc) { + #if QT_CONFIG(slider) + case CC_MeterSlider: + if (const QStyleOptionSlider *slider = qstyleoption_cast(option)) { + const qreal dpr = painter->device()->devicePixelRatio(); + const QColor buttonColor = this->buttonColor(option->palette); + QRect groove = this->subControlRect(static_cast(CC_MeterSlider), option, SC_SliderGroove, widget); + QRect handle = this->subControlRect(static_cast(CC_MeterSlider), option, SC_SliderHandle, widget); + + bool horizontal = slider->orientation == Qt::Horizontal; + bool ticksAbove = slider->tickPosition & QSlider::TicksAbove; + bool ticksBelow = slider->tickPosition & QSlider::TicksBelow; + QColor activeHighlight = this->highlight(option->palette); + QPixmap cache; + QBrush oldBrush = painter->brush(); + QPen oldPen = painter->pen(); + QColor shadowAlpha(Qt::black); + shadowAlpha.setAlpha(10); + QColor outline; + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) + outline = this->highlightedOutline(option->palette); + + if ((option->subControls & SC_SliderGroove) && groove.isValid()) { + QColor grooveColor; + grooveColor.setHsv(buttonColor.hue(), + qMin(255, (int)(buttonColor.saturation())), + qMin(255, (int)(buttonColor.value()*0.9))); + QString groovePixmapName = "slider_groove" + QString::number(groove.width());//QStyleHelper::uniqueName("slider_groove"_L1, option, groove.size(), dpr); + QRect pixmapRect(0, 0, groove.width(), groove.height()); + + // draw background groove + if (!QPixmapCache::find(groovePixmapName, &cache)) { + cache = this->styleCachePixmap(pixmapRect.size(), dpr); + QPainter groovePainter(&cache); + groovePainter.setRenderHint(QPainter::Antialiasing, true); + groovePainter.translate(0.5, 0.5); + QLinearGradient gradient; + if (horizontal) { + gradient.setStart(pixmapRect.center().x(), pixmapRect.top()); + gradient.setFinalStop(pixmapRect.center().x(), pixmapRect.bottom()); + } + else { + gradient.setStart(pixmapRect.left(), pixmapRect.center().y()); + gradient.setFinalStop(pixmapRect.right(), pixmapRect.center().y()); + } + groovePainter.setPen(QPen(outline)); + gradient.setColorAt(0, grooveColor.darker(110)); + gradient.setColorAt(1, grooveColor.lighter(110));//palette.button().color().darker(115)); + groovePainter.setBrush(gradient); + groovePainter.drawRoundedRect(pixmapRect.adjusted(1, 1, -2, -2), 1, 1); + groovePainter.end(); + QPixmapCache::insert(groovePixmapName, cache); + } + painter->drawPixmap(groove.topLeft(), cache); + + // draw groove lines (vol + unattenuated vol) + const MeterSlider* widgetCast = qobject_cast(widget); + if (widgetCast) { + QRect clipRect; + //if (!groovePixmapName.isEmpty()) groovePixmapName += QLatin1String("_unattenuated"); + //groovePixmapName += "_unattenuated"_L1; + //lf (!QPixmapCache::find(groovePixmapName, &cache)) { + cache = styleCachePixmap(pixmapRect.size(), dpr); + QPainter groovePainter(&cache); + QLinearGradient gradient; + if (horizontal) { + gradient.setStart(pixmapRect.center().x(), pixmapRect.top()); + gradient.setFinalStop(pixmapRect.center().x(), pixmapRect.bottom()); + } + else { + gradient.setStart(pixmapRect.left(), pixmapRect.center().y()); + gradient.setFinalStop(pixmapRect.right(), pixmapRect.center().y()); + } + QColor highlight = QColor(30,30,30,100); //this->highlight(option->palette); + QColor highlightedoutline = highlight.darker(140); + QColor grooveOutline = outline; + if (qGray(grooveOutline.rgb()) > qGray(highlightedoutline.rgb())) + grooveOutline = highlightedoutline; + + float peakValue = ((MeterSlider*)widget)->peakValue; + groovePainter.setRenderHint(QPainter::Antialiasing, true); + groovePainter.translate(0.5, 0.5); + groovePainter.setPen(QPen(grooveOutline)); + gradient.setColorAt(0, highlight); + gradient.setColorAt(1, highlight.lighter(130)); + groovePainter.setBrush(gradient); + groovePainter.drawRoundedRect(pixmapRect.adjusted( + 1, 1, + -pixmapRect.width() + (pixmapRect.width() * peakValue), + -2), 1, 1); + groovePainter.setPen(this->innerContrastLine()); + groovePainter.setBrush(Qt::green); + double stepWidth = (double)widgetCast->width() * ((double)widgetCast->singleStep() / (widgetCast->maximum() - widgetCast->minimum())); + groovePainter.drawRoundedRect(pixmapRect.adjusted( + 1, 1, + -pixmapRect.width() + ((widgetCast->width() - ((widgetCast->maximum() - widgetCast->value()) * stepWidth)) * peakValue), + -2), 1, 1); + + groovePainter.end(); + //QPixmapCache::insert(groovePixmapName, cache); + //} + //} + if (horizontal) { + if (slider->upsideDown) + clipRect = QRect(handle.right(), groove.top(), groove.right() - handle.right(), groove.height()); + else + clipRect = QRect(groove.left() + 2, groove.top() + 2, + groove.right() - 2, 2); + } else { + if (slider->upsideDown) + clipRect = QRect(groove.left(), handle.bottom(), groove.width(), groove.height() - (handle.bottom() - slider->rect.top())); + else + clipRect = QRect(groove.left(), groove.top(), groove.width(), handle.top() - groove.top()); + } + painter->save(); + painter->setClipRect(clipRect.adjusted(0, 0, 1, 1), Qt::IntersectClip); + painter->drawPixmap(groove.topLeft(), cache); + painter->restore(); + } + } + if (option->subControls & SC_SliderTickmarks) { + painter->save(); + painter->translate(slider->rect.x(), slider->rect.y()); + painter->setPen(outline); + int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); + int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + int interval = slider->tickInterval; + if (interval <= 0) { + interval = slider->singleStep; + if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, + available) + - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + 0, available) < 3) + interval = slider->pageStep; + } + if (interval <= 0) + interval = 1; + + int v = slider->minimum; + int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + QVector lines; + while (v <= slider->maximum + 1) { + if (v == slider->maximum + 1 && interval == 1) + break; + const int v_ = qMin(v, slider->maximum); + int pos = sliderPositionFromValue(slider->minimum, slider->maximum, + v_, (horizontal + ? slider->rect.width() + : slider->rect.height()) - len, + slider->upsideDown) + len / 2; + int extra = 2 - ((v_ == slider->minimum || v_ == slider->maximum) ? 1 : 0); + + if (horizontal) { + if (ticksAbove) { + lines += QLine(pos, slider->rect.top() + extra, + pos, slider->rect.top() + tickSize); + } + if (ticksBelow) { + lines += QLine(pos, slider->rect.bottom() - extra, + pos, slider->rect.bottom() - tickSize); + } + } else { + if (ticksAbove) { + lines += QLine(slider->rect.left() + extra, pos, + slider->rect.left() + tickSize, pos); + } + if (ticksBelow) { + lines += QLine(slider->rect.right() - extra, pos, + slider->rect.right() - tickSize, pos); + } + } + // in the case where maximum is max int + int nextInterval = v + interval; + if (nextInterval < v) + break; + v = nextInterval; + } + painter->drawLines(lines); + painter->restore(); + } + // draw handle + if ((option->subControls & SC_SliderHandle) ) { + QString handlePixmapName = "slider_handle" + QString::number(handle.height());//QStyleHelper::uniqueName("slider_handle"_L1, option,//handle.size(), dpr); + if (!QPixmapCache::find(handlePixmapName, &cache)) { + cache = styleCachePixmap(handle.size(), dpr); + QRect pixmapRect(0, 0, handle.width(), handle.height()); + QPainter handlePainter(&cache); + QRect gradRect = pixmapRect.adjusted(2, 2, -2, -2); + + // gradient fill + QRect r = pixmapRect.adjusted(1, 1, -2, -2); + QLinearGradient gradient = qt_fusion_gradient(gradRect, this->buttonColor(option->palette),horizontal ? TopDown : FromLeft); + + handlePainter.setRenderHint(QPainter::Antialiasing, true); + handlePainter.translate(0.5, 0.5); + + handlePainter.setPen(Qt::NoPen); + handlePainter.setBrush(QColor(0, 0, 0, 40)); + handlePainter.drawRect(horizontal ? r.adjusted(-1, 2, 1, -2) : r.adjusted(2, -1, -2, 1)); + + handlePainter.setPen(QPen(this->outline(option->palette))); + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) + handlePainter.setPen(QPen(this->highlightedOutline(option->palette))); + + handlePainter.setBrush(gradient); + handlePainter.drawRoundedRect(r, 2, 2); + handlePainter.setBrush(Qt::NoBrush); + handlePainter.setPen(this->innerContrastLine()); + handlePainter.drawRoundedRect(r.adjusted(1, 1, -1, -1), 2, 2); + + QColor cornerAlpha = outline.darker(120); + cornerAlpha.setAlpha(80); + + //handle shadow + handlePainter.setPen(shadowAlpha); + handlePainter.drawLine(QPoint(r.left() + 2, r.bottom() + 1), QPoint(r.right() - 2, r.bottom() + 1)); + handlePainter.drawLine(QPoint(r.right() + 1, r.bottom() - 3), QPoint(r.right() + 1, r.top() + 4)); + handlePainter.drawLine(QPoint(r.right() - 1, r.bottom()), QPoint(r.right() + 1, r.bottom() - 2)); + + handlePainter.end(); + QPixmapCache::insert(handlePixmapName, cache); + } + + painter->drawPixmap(handle.topLeft(), cache); + + } + painter->setBrush(oldBrush); + painter->setPen(oldPen); + } + break; + default: + baseStyle()->drawComplexControl(cc, option, painter, widget); +#endif // QT_CONFIG(slider) +} +} + +private: + enum Direction { + TopDown, + FromLeft, + BottomUp, + FromRight + }; + + QColor highlightedOutline(const QPalette &pal) const { + QColor highlightedOutline = highlight(pal).darker(125); + if (highlightedOutline.value() > 160) + highlightedOutline.setHsl(highlightedOutline.hue(), highlightedOutline.saturation(), 160); + return highlightedOutline; + } + + QColor outline(const QPalette &pal) const { + if (pal.window().style() == Qt::TexturePattern) + return QColor(0, 0, 0, 160); + return pal.window().color().darker(140); + } + + QColor buttonColor(const QPalette &pal) const { + QColor buttonColor = pal.button().color(); + int val = qGray(buttonColor.rgb()); + buttonColor = buttonColor.lighter(100 + qMax(1, (180 - val)/6)); + buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, buttonColor.value()); + return buttonColor; + } + + QColor highlight(const QPalette &pal) const { + if (isMacSystemPalette(pal)) + return QColor(60, 140, 230); + return pal.color(QPalette::Highlight); + } + + QColor innerContrastLine() const { + return QColor(255, 255, 255, 30); + } + + bool isMacSystemPalette(const QPalette &pal) const { + Q_UNUSED(pal); +#if defined(Q_OS_MACOS) + const QPalette *themePalette = QGuiApplicationPrivate::platformTheme()->palette(); + if (themePalette && themePalette->color(QPalette::Normal, QPalette::Highlight) == + pal.color(QPalette::Normal, QPalette::Highlight) && + themePalette->color(QPalette::Normal, QPalette::HighlightedText) == + pal.color(QPalette::Normal, QPalette::HighlightedText)) + return true; +#endif + return false; + } + + inline QPixmap styleCachePixmap(const QSize &size, qreal pixelRatio) const { + //not api + QPixmap cachePixmap = QPixmap(size * pixelRatio); + cachePixmap.setDevicePixelRatio(pixelRatio); + cachePixmap.fill(Qt::transparent); + return cachePixmap; + } + + static QLinearGradient qt_fusion_gradient(const QRect &rect, const QBrush &baseColor, Direction direction = TopDown) +{ + int x = rect.center().x(); + int y = rect.center().y(); + QLinearGradient gradient; + switch (direction) { + case FromLeft: + gradient = QLinearGradient(rect.left(), y, rect.right(), y); + break; + case FromRight: + gradient = QLinearGradient(rect.right(), y, rect.left(), y); + break; + case BottomUp: + gradient = QLinearGradient(x, rect.bottom(), x, rect.top()); + break; + case TopDown: + default: + gradient = QLinearGradient(x, rect.top(), x, rect.bottom()); + break; + } + if (baseColor.gradient()) + gradient.setStops(baseColor.gradient()->stops()); + else { + QColor gradientStartColor = baseColor.color().lighter(124); + QColor gradientStopColor = baseColor.color().lighter(102); + gradient.setColorAt(0, gradientStartColor); + gradient.setColorAt(1, gradientStopColor); + // Uncomment for adding shiny shading + // QColor midColor1 = mergedColors(gradientStartColor, gradientStopColor, 55); + // QColor midColor2 = mergedColors(gradientStartColor, gradientStopColor, 45); + // gradient.setColorAt(0.5, midColor1); + // gradient.setColorAt(0.501, midColor2); + } + return gradient; +} + }; + /* * void MixerStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, - * QPainter *p, const QWidget *widget) const { - * #if QT_CONFIG(slider) - * case CC_Slider: - * if (const QStyleOptionSlider *slider = qstyleoption_cast(opt)) { - * if (slider->subControls == SC_SliderTickmarks) { - * int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget); - * int ticks = slider->tickPosition; - * int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget); - * int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); - * int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); - * int interval = slider->tickInterval; - * if (interval <= 0) { - * interval = slider->singleStep; - * if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, - * available) - * - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, - * 0, available) < 3) - * interval = slider->pageStep; - * } - * if (!interval) - * interval = 1; - * int fudge = len / 2; - * int pos; - * // Since there is no subrect for tickmarks do a translation here. - * QPainterStateSaver pss(p); - * p->translate(slider->rect.x(), slider->rect.y()); - * p->setPen(slider->palette.windowText().color()); - * int v = slider->minimum; - * while (v <= slider->maximum + 1) { - * if (v == slider->maximum + 1 && interval == 1) - * break; - * const int v_ = qMin(v, slider->maximum); - * pos = QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, - * v_, available) + fudge; - ** - * if (slider->orientation == Qt::Horizontal) { - * if (ticks & QSlider::TicksAbove) - * p->drawLine(pos, 0, pos, tickOffset - 2); - * if (ticks & QSlider::TicksBelow) - * p->drawLine(pos, tickOffset + thickness + 1, pos, - * slider->rect.height()-1); - * } else { - * if (ticks & QSlider::TicksAbove) - * p->drawLine(0, pos, tickOffset - 2, pos); - * if (ticks & QSlider::TicksBelow) - * p->drawLine(tickOffset + thickness + 1, pos, - * slider->rect.width()-1, pos); - * } - * // in the case where maximum is max int - * int nextInterval = v + interval; - * if (nextInterval < v) - * break; - * v = nextInterval; - * } - * } - * } - * break; -*#endif // QT_CONFIG(slider) -*/ -//} + * QPainter *p, const QWidget *widget) const { + * switch (cc) { + * #if QT_CONFIG(slider) + * case CC_Slider: + * if (const QStyleOptionSlider *slider = qstyleoption_cast(opt)) { + * if (slider->subControls == SC_SliderTickmarks) { + * int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget); + * int ticks = slider->tickPosition; + * int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget); + * int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + * int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + * int interval = slider->tickInterval; + * if (interval <= 0) { + * interval = slider->singleStep; + * if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, + * available) + * - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + * 0, available) < 3) + * interval = slider->pageStep; + * } + * if (!interval) + * interval = 1; + * int fudge = len / 2; + * int pos; + * // Since there is no subrect for tickmarks do a translation here. + * p->save(); + * p->translate(slider->rect.x(), slider->rect.y()); + * p->setPen(slider->palette.windowText().color()); + * int v = slider->minimum; + * while (v <= slider->maximum + 1) { + * if (v == slider->maximum + 1 && interval == 1) + * break; + * const int v_ = qMin(v, slider->maximum); + * pos = QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + * v_, available) + fudge; + * if (slider->orientation == Qt::Horizontal) { + * if (ticks & QSlider::TicksAbove) + * p->drawLine(pos, 0, pos, tickOffset - 2); + * if (ticks & QSlider::TicksBelow) + * p->drawLine(pos, tickOffset + thickness + 1, pos, + * slider->rect.height()-1); + * } else { + * if (ticks & QSlider::TicksAbove) + * p->drawLine(0, pos, tickOffset - 2, pos); + * if (ticks & QSlider::TicksBelow) + * p->drawLine(tickOffset + thickness + 1, pos, + * slider->rect.width()-1, pos); + * } + * // in the case where maximum is max int + * int nextInterval = v + interval; + * if (nextInterval < v) + * break; + * v = nextInterval; + * } + * } + * } + * p->restore(); + * break; + * default: + * baseStyle()->drawComplexControl(cc, opt, p, widget); + * break; + * } + * #endif // QT_CONFIG(slider) + * } + */ + +//void MixerStyle:: diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index e4a6aec..f37e337 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -7,6 +7,7 @@ #include #include #include +#include //#include "contclasses.h" #define INIT_FILELOG #include "qtclasses.h" @@ -54,7 +55,11 @@ int main (int argc, char* argv[]) { * } */ - QApplication::setStyle(new MixerStyle(QStyleFactory::create("windowsvista"))); + QApplication::setStyle(new MixerStyle(QStyleFactory::create("Fusion"))); + QPalette palette = QGuiApplication::palette(); + //todo: ez full apply os accent colorw + palette.setColor(QPalette::Active, QPalette::Highlight, QColor(255, 192, 203, 200));//QColor(30,30,30,100)); + QGuiApplication::setPalette(palette); //Check if running //https://stackoverflow.com/questions/48060989/qt-show-application-if-currently-running initialize_file_log(); From f7de5ef80351f4244acc12afd002153413013fea Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 30 May 2024 19:23:09 +0200 Subject: [PATCH 06/38] Qt code refactor w/ functional style --- qtest.pro | 2 +- src/qt/meterslider.h | 19 +++++ src/qt/qtclasses.cpp | 2 +- src/qt/qtclasses.h | 65 ++--------------- src/qt/qtcommon.h | 165 +++++++++++++++++++++++++++++++++++++++++++ src/qt/qtvisuals.h | 141 ++++-------------------------------- src/qtestmain.cpp | 14 +--- 7 files changed, 207 insertions(+), 201 deletions(-) create mode 100644 src/qt/meterslider.h create mode 100644 src/qt/qtcommon.h diff --git a/qtest.pro b/qtest.pro index 63ef85f..303f059 100644 --- a/qtest.pro +++ b/qtest.pro @@ -11,7 +11,7 @@ DESTPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" SOURCES += qtestmain.cpp qtclasses.cpp backlasses.cpp backsessionclasses.cpp contclasses.cpp contsessionclasses.cpp -HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h +HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h meterslider.h qtvisuals.h RESOURCES = assets.qrc RC_ICONS += assets/logo.ico diff --git a/src/qt/meterslider.h b/src/qt/meterslider.h new file mode 100644 index 0000000..a59e16e --- /dev/null +++ b/src/qt/meterslider.h @@ -0,0 +1,19 @@ +#include + +class MeterSlider : public QSlider { + Q_OBJECT +private: + ~MeterSlider(); + float peakValue; + + friend class MixerStyle; +protected: + bool event(QEvent* ev) override; + void paintEvent(QPaintEvent *event) override; + +public: + //MeterSlider(Qt::Orientation orientation, QWidget *parent = nullptr); + //MeterSlider(QWidget* parent = nullptr) : MeterSlider(Qt::Vertical, parent){}; + void setPeakValue(float peakValue); + using QSlider::QSlider; +}; diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 6e90375..d0141ea 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -1,4 +1,5 @@ #include "qtclasses.h" +#include "meterslider.h" #define POLLING_RATE 2 template @@ -103,7 +104,6 @@ void MeterSlider::paintEvent(QPaintEvent *event) { QPainter painter(this); QStyle* stle = QApplication::style(); stle->drawComplexControl((QStyle::ComplexControl)CC_MeterSlider, &sliderComplex2, &painter, this); - //Q_D(QSlider); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 47216be..6a5e9aa 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -1,50 +1,10 @@ #pragma once -#include -#include -#include - -#include -#include -#include -//#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -//#include -/* - * #else - * class QSlider; - * class QLabel; - * class QGridLayout; - * class QPushButton; - * class QWidget; - * class QMainWindow; - * #endif - */ - -#include "global.h" +#include "qtcommon.h" #include "contclasses.h" +class MeterSlider; + enum SpawnPos { LEFT = (1 << 1), RIGHT = (0 << 1), @@ -70,26 +30,9 @@ public: }; //Q_DECLARE_METATYPE(EndpointWidgetEvent) -class MeterSlider : public QSlider { - Q_OBJECT -private: - ~MeterSlider(); - float peakValue; - - friend class MixerStyle; -protected: - bool event(QEvent* ev) override; - void paintEvent(QPaintEvent *event) override; - -public: - //MeterSlider(Qt::Orientation orientation, QWidget *parent = nullptr); - //MeterSlider(QWidget* parent = nullptr) : MeterSlider(Qt::Vertical, parent){}; - void setPeakValue(float peakValue); - using QSlider::QSlider; -}; //todo: TEST. TEST. -#include "qtvisuals.h" +//#include "qtvisuals.h" class ExtendedCheckBox : public QCheckBox { Q_OBJECT diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h new file mode 100644 index 0000000..8c9119c --- /dev/null +++ b/src/qt/qtcommon.h @@ -0,0 +1,165 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +//#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//#include +/* + * #else + * class QSlider; + * class QLabel; + * class QGridLayout; + * class QPushButton; + * class QWidget; + * class QMainWindow; + * #endif + */ + +#include "global.h" + +enum CustomComplexControl { + CC_MeterSlider = 0xf0000001 +}; + +namespace StylingHelper { + static inline QLatin1String operator""_L1(const char* ch, uint64_t) { + return QLatin1String(ch); + } + + static inline qreal dpi(const QStyleOption *option) { + #ifndef Q_OS_DARWIN + // Prioritize the application override, except for on macOS where + // we have historically not supported the AA_Use96Dpi flag. + if (QCoreApplication::testAttribute(Qt::AA_Use96Dpi)) + return 96; + #endif + + // Expect that QStyleOption::QFontMetrics::QFont has the correct DPI set + if (option) + return option->fontMetrics.fontDpi(); + + // Fall back to historical Qt behavior: hardocded 72 DPI on mac, + // primary screen DPI on other platforms. + #ifdef Q_OS_DARWIN + return qstyleBaseDpi; + #else + return QGuiApplication::primaryScreen()->physicalDotsPerInch(); + #endif + } + + /* + * #ifdef Q_OS_DARWIN + * static const qreal qstyleBaseDpi = 72; + * #else + * static const qreal qstyleBaseDpi = 96; + * #endif + */ + + //Q_GUI_EXPORT int qt_defaultDpiX() const; + + + static inline qreal dpiScaled(qreal value, qreal dpi) { + static const qreal qstyleBaseDpi = 96; + return value * dpi / qstyleBaseDpi; + } + + static inline qreal dpiScaled(qreal value, const QPaintDevice *device) { + return dpiScaled(value, device->logicalDpiX()); + } + + static inline qreal dpiScaled(qreal value, const QStyleOption *option) { + return dpiScaled(value, dpi(option)); + } + + static inline bool isMacSystemPalette(const QPalette &pal) { + Q_UNUSED(pal); +#if defined(Q_OS_MACOS) + const QPalette *themePalette = QGuiApplicationPrivate::platformTheme()->palette(); + if (themePalette && themePalette->color(QPalette::Normal, QPalette::Highlight) == + pal.color(QPalette::Normal, QPalette::Highlight) && + themePalette->color(QPalette::Normal, QPalette::HighlightedText) == + pal.color(QPalette::Normal, QPalette::HighlightedText)) + return true; +#endif + return false; + } + + static inline QColor highlight(const QPalette &pal) { + if (isMacSystemPalette(pal)) + return QColor(60, 140, 230); + return pal.color(QPalette::Highlight); + } + + static inline QColor highlightedOutline(const QPalette &pal) { + QColor highlightedOutline = highlight(pal).darker(125); + if (highlightedOutline.value() > 160) + highlightedOutline.setHsl(highlightedOutline.hue(), highlightedOutline.saturation(), 160); + return highlightedOutline; + } + + static inline QColor getOutline(const QPalette &pal) { + if (pal.window().style() == Qt::TexturePattern) + return QColor(0, 0, 0, 160); + return pal.window().color().darker(140); + } + + static inline QColor getButtonColor(const QPalette &pal) { + QColor buttonColor = pal.button().color(); + int val = qGray(buttonColor.rgb()); + buttonColor = buttonColor.lighter(100 + qMax(1, (180 - val)/6)); + buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, buttonColor.value()); + return buttonColor; + } + + static inline QColor innerContrastLine() { + return QColor(255, 255, 255, 30); + } + + static inline QPixmap styleCachePixmap(const QSize &size, qreal pixelRatio) { + //not api + QPixmap cachePixmap = QPixmap(size * pixelRatio); + cachePixmap.setDevicePixelRatio(pixelRatio); + cachePixmap.fill(Qt::transparent); + return cachePixmap; + } +} + diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h index bad98bb..f88c025 100644 --- a/src/qt/qtvisuals.h +++ b/src/qt/qtvisuals.h @@ -1,28 +1,9 @@ #pragma once -#include -#include -#include -#include -//#include "qstylehelper.cpp" - -//repeats. bruh. -//#include -//#include - -enum CustomComplexControl { - CC_MeterSlider = 0xf0000001 -}; - - -namespace qt64 { - static inline QLatin1String operator""_L1(const char* ch, uint64_t) { - return QLatin1String(ch); - } -} - -using namespace qt64; +#include "qtcommon.h" +#include "meterslider.h" +using namespace StylingHelper; class MixerStyle : public QProxyStyle { public: @@ -38,50 +19,6 @@ public: return QProxyStyle::styleHint(hint, option, widget, returnData); } - /* - * #ifdef Q_OS_DARWIN - * static const qreal qstyleBaseDpi = 72; - * #else - * static const qreal qstyleBaseDpi = 96; - * #endif - */ - - //Q_GUI_EXPORT int qt_defaultDpiX() const; - - qreal dpi(const QStyleOption *option) const { - #ifndef Q_OS_DARWIN - // Prioritize the application override, except for on macOS where - // we have historically not supported the AA_Use96Dpi flag. - if (QCoreApplication::testAttribute(Qt::AA_Use96Dpi)) - return 96; - #endif - - // Expect that QStyleOption::QFontMetrics::QFont has the correct DPI set - if (option) - return option->fontMetrics.fontDpi(); - - // Fall back to historical Qt behavior: hardocded 72 DPI on mac, - // primary screen DPI on other platforms. - #ifdef Q_OS_DARWIN - return qstyleBaseDpi; - #else - return QGuiApplication::primaryScreen()->physicalDotsPerInch(); - #endif - } - - qreal dpiScaled(qreal value, qreal dpi) const { - static const qreal qstyleBaseDpi = 96; - return value * dpi / qstyleBaseDpi; - } - - qreal dpiScaled(qreal value, const QPaintDevice *device) const { - return dpiScaled(value, device->logicalDpiX()); - } - - qreal dpiScaled(qreal value, const QStyleOption *option) const { - return dpiScaled(value, dpi(option)); - } - QRect subControlRect(ComplexControl control, const QStyleOptionComplex *option, SubControl subControl, const QWidget *widget) const { QRect rect = QCommonStyle::subControlRect(CC_Slider, option, subControl, widget); @@ -122,7 +59,7 @@ public: case SC_SliderGroove: { QPoint grooveCenter = slider->rect.center(); //rect.setWidth(std::abs(grooveCenter.x()) * 2); - const int grooveThickness = this->dpiScaled(7, option); //QStyleHelper::dpiScaled(7, option); + const int grooveThickness = dpiScaled(7, option); //QStyleHelper::dpiScaled(7, option); const bool bothTicks = (slider->tickPosition & QSlider::TicksBothSides) == QSlider::TicksBothSides; if (slider->orientation == Qt::Horizontal) { rect.setHeight(grooveThickness); @@ -165,14 +102,14 @@ public: case CC_MeterSlider: if (const QStyleOptionSlider *slider = qstyleoption_cast(option)) { const qreal dpr = painter->device()->devicePixelRatio(); - const QColor buttonColor = this->buttonColor(option->palette); + const QColor buttonColor = getButtonColor(option->palette); QRect groove = this->subControlRect(static_cast(CC_MeterSlider), option, SC_SliderGroove, widget); QRect handle = this->subControlRect(static_cast(CC_MeterSlider), option, SC_SliderHandle, widget); bool horizontal = slider->orientation == Qt::Horizontal; bool ticksAbove = slider->tickPosition & QSlider::TicksAbove; bool ticksBelow = slider->tickPosition & QSlider::TicksBelow; - QColor activeHighlight = this->highlight(option->palette); + QColor activeHighlight = highlight(option->palette); QPixmap cache; QBrush oldBrush = painter->brush(); QPen oldPen = painter->pen(); @@ -180,7 +117,7 @@ public: shadowAlpha.setAlpha(10); QColor outline; if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) - outline = this->highlightedOutline(option->palette); + outline = highlightedOutline(option->palette); if ((option->subControls & SC_SliderGroove) && groove.isValid()) { QColor grooveColor; @@ -192,7 +129,7 @@ public: // draw background groove if (!QPixmapCache::find(groovePixmapName, &cache)) { - cache = this->styleCachePixmap(pixmapRect.size(), dpr); + cache = styleCachePixmap(pixmapRect.size(), dpr); QPainter groovePainter(&cache); groovePainter.setRenderHint(QPainter::Antialiasing, true); groovePainter.translate(0.5, 0.5); @@ -250,7 +187,7 @@ public: 1, 1, -pixmapRect.width() + (pixmapRect.width() * peakValue), -2), 1, 1); - groovePainter.setPen(this->innerContrastLine()); + groovePainter.setPen(innerContrastLine()); groovePainter.setBrush(Qt::green); double stepWidth = (double)widgetCast->width() * ((double)widgetCast->singleStep() / (widgetCast->maximum() - widgetCast->minimum())); groovePainter.drawRoundedRect(pixmapRect.adjusted( @@ -351,7 +288,7 @@ public: // gradient fill QRect r = pixmapRect.adjusted(1, 1, -2, -2); - QLinearGradient gradient = qt_fusion_gradient(gradRect, this->buttonColor(option->palette),horizontal ? TopDown : FromLeft); + QLinearGradient gradient = qt_fusion_gradient(gradRect, getButtonColor(option->palette),horizontal ? TopDown : FromLeft); handlePainter.setRenderHint(QPainter::Antialiasing, true); handlePainter.translate(0.5, 0.5); @@ -360,14 +297,14 @@ public: handlePainter.setBrush(QColor(0, 0, 0, 40)); handlePainter.drawRect(horizontal ? r.adjusted(-1, 2, 1, -2) : r.adjusted(2, -1, -2, 1)); - handlePainter.setPen(QPen(this->outline(option->palette))); + handlePainter.setPen(QPen(getOutline(option->palette))); if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) - handlePainter.setPen(QPen(this->highlightedOutline(option->palette))); + handlePainter.setPen(QPen(highlightedOutline(option->palette))); handlePainter.setBrush(gradient); handlePainter.drawRoundedRect(r, 2, 2); handlePainter.setBrush(Qt::NoBrush); - handlePainter.setPen(this->innerContrastLine()); + handlePainter.setPen(innerContrastLine()); handlePainter.drawRoundedRect(r.adjusted(1, 1, -1, -1), 2, 2); QColor cornerAlpha = outline.darker(120); @@ -404,57 +341,7 @@ private: FromRight }; - QColor highlightedOutline(const QPalette &pal) const { - QColor highlightedOutline = highlight(pal).darker(125); - if (highlightedOutline.value() > 160) - highlightedOutline.setHsl(highlightedOutline.hue(), highlightedOutline.saturation(), 160); - return highlightedOutline; - } - - QColor outline(const QPalette &pal) const { - if (pal.window().style() == Qt::TexturePattern) - return QColor(0, 0, 0, 160); - return pal.window().color().darker(140); - } - - QColor buttonColor(const QPalette &pal) const { - QColor buttonColor = pal.button().color(); - int val = qGray(buttonColor.rgb()); - buttonColor = buttonColor.lighter(100 + qMax(1, (180 - val)/6)); - buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, buttonColor.value()); - return buttonColor; - } - - QColor highlight(const QPalette &pal) const { - if (isMacSystemPalette(pal)) - return QColor(60, 140, 230); - return pal.color(QPalette::Highlight); - } - - QColor innerContrastLine() const { - return QColor(255, 255, 255, 30); - } - - bool isMacSystemPalette(const QPalette &pal) const { - Q_UNUSED(pal); -#if defined(Q_OS_MACOS) - const QPalette *themePalette = QGuiApplicationPrivate::platformTheme()->palette(); - if (themePalette && themePalette->color(QPalette::Normal, QPalette::Highlight) == - pal.color(QPalette::Normal, QPalette::Highlight) && - themePalette->color(QPalette::Normal, QPalette::HighlightedText) == - pal.color(QPalette::Normal, QPalette::HighlightedText)) - return true; -#endif - return false; - } - - inline QPixmap styleCachePixmap(const QSize &size, qreal pixelRatio) const { - //not api - QPixmap cachePixmap = QPixmap(size * pixelRatio); - cachePixmap.setDevicePixelRatio(pixelRatio); - cachePixmap.fill(Qt::transparent); - return cachePixmap; - } + static QLinearGradient qt_fusion_gradient(const QRect &rect, const QBrush &baseColor, Direction direction = TopDown) { diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index f37e337..6c435c3 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -1,17 +1,9 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include //#include "contclasses.h" #define INIT_FILELOG +#include "qtcommon.h" #include "qtclasses.h" -#include "global.h" +#include "qtvisuals.h" +//#include "global.h" OverseerHandler *osh = nullptr; From 313a26c8e3f6361b86548e032071043ffb64f84e Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 30 May 2024 22:36:35 +0200 Subject: [PATCH 07/38] wip: customize scroll bar --- src/qt/qtclasses.cpp | 18 +- src/qt/qtcommon.h | 25 ++- src/qt/qtvisuals.h | 455 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 449 insertions(+), 49 deletions(-) diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index d0141ea..d6b4c48 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -250,6 +250,8 @@ void MainWindow::compose() { log_debugcpp("Window Width: " + std::to_string(windowWidth)); this->createLayout(new QGridLayout()); + //scrollArea->verticalScrollBar()->setMinimumWidth(windowWidth * (widthRatio)); + //scrollArea->verticalScrollBar()->setMaximumWidth(windowWidth * (widthRatio)); /* * this->setAttribute(Qt::WA_DontShowOnScreen, true); * this->show(); @@ -257,10 +259,11 @@ void MainWindow::compose() { * this->hide(); * this->setAttribute(Qt::WA_DontShowOnScreen, false); */ - + const qreal dpr = screen->devicePixelRatio(); + log_to_file("dpr: %f \n", dpr); for (auto *epw : ews) { if (!epw) continue; - epw->calculateSize(windowWidth, screenHeight); + epw->calculateSize(windowWidth * dpr,screenHeight * screen->devicePixelRatio()); log_debugcpp("epw loop"); log_debugcpp("epw roles: " + print_as_binary((epw->getEndpointHandler()->getRoles()))); //std::bitset content = @@ -277,8 +280,8 @@ void MainWindow::compose() { if (!ews[i]) continue; ews[i]->layout()->getContentsMargins(&left, &top, &right, &bottom); windowHeight += ews[i]->sizeHint().height(); - windowHeight += top; - windowHeight += bottom; + windowHeight += top * 3; + //windowHeight += bottom; log_debugcpp("windowHeight loop: " + std::to_string(windowHeight)); } windowHeight += mainMenuBar->sizeHint().height(); @@ -941,13 +944,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { scrollArea = new QScrollArea(this); scrollArea->setWidget(widget); scrollArea->setWidgetResizable(true); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setContentsMargins(QMargins(0, 0, 0, 0)); + //scrollArea->verticalScrollBar()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); //scrollArea->verticalScrollBar()->setSingleStep(1); - //custom style = no qss - //scrollArea->setStyleSheet("QScrollBar:vertical { width: 4px; }"); - //scrollArea->setMinimumWidth(500); setCentralWidget(scrollArea); /* diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h index 8c9119c..dc2797b 100644 --- a/src/qt/qtcommon.h +++ b/src/qt/qtcommon.h @@ -41,7 +41,7 @@ #include #include #include - +//#include //#include /* * #else @@ -65,7 +65,7 @@ namespace StylingHelper { return QLatin1String(ch); } - static inline qreal dpi(const QStyleOption *option) { + static inline qreal calculateDpi(const QStyleOption *option) { #ifndef Q_OS_DARWIN // Prioritize the application override, except for on macOS where // we have historically not supported the AA_Use96Dpi flag. @@ -77,7 +77,7 @@ namespace StylingHelper { if (option) return option->fontMetrics.fontDpi(); - // Fall back to historical Qt behavior: hardocded 72 DPI on mac, + // Fall back to historical Qt behavior: hardcoded 72 DPI on mac, // primary screen DPI on other platforms. #ifdef Q_OS_DARWIN return qstyleBaseDpi; @@ -86,6 +86,10 @@ namespace StylingHelper { #endif } + static inline qreal calculateDpi(const QScreen screen) { + return QFontMetrics(QApplication::font()).fontDpi(); + } + /* * #ifdef Q_OS_DARWIN * static const qreal qstyleBaseDpi = 72; @@ -107,7 +111,7 @@ namespace StylingHelper { } static inline qreal dpiScaled(qreal value, const QStyleOption *option) { - return dpiScaled(value, dpi(option)); + return dpiScaled(value, calculateDpi(option)); } static inline bool isMacSystemPalette(const QPalette &pal) { @@ -161,5 +165,18 @@ namespace StylingHelper { cachePixmap.fill(Qt::transparent); return cachePixmap; } + + static inline QColor backgroundColor(const QPalette &pal, const QWidget* widget) + { + #if QT_CONFIG(scrollarea) + if (qobject_cast(widget) && widget->parent() && + qobject_cast(widget->parent()->parent())) + return widget->parentWidget()->parentWidget()->palette().color(QPalette::Base); + #else + Q_UNUSED(widget); + #endif + return pal.color(QPalette::Base); + } + } diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h index f88c025..4c7ac4f 100644 --- a/src/qt/qtvisuals.h +++ b/src/qt/qtvisuals.h @@ -16,15 +16,15 @@ public: QStyleHintReturn* returnData = 0) const { if (hint == QStyle::SH_Slider_AbsoluteSetButtons) return (Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); - return QProxyStyle::styleHint(hint, option, widget, returnData); + return baseStyle()->styleHint(hint, option, widget, returnData); } QRect subControlRect(ComplexControl control, const QStyleOptionComplex *option, - SubControl subControl, const QWidget *widget) const { + SubControl subControl, const QWidget *widget) const { QRect rect = QCommonStyle::subControlRect(CC_Slider, option, subControl, widget); switch (control) { - #if QT_CONFIG(slider) +#if QT_CONFIG(slider) case CC_MeterSlider: if (const QStyleOptionSlider *slider = qstyleoption_cast(option)) { int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); @@ -88,6 +88,94 @@ public: return rect; break; #endif // QT_CONFIG(slider) +#if QT_CONFIG(scrollbar) + case CC_ScrollBar: + if (const QStyleOptionSlider *scrollbar = qstyleoption_cast(option)) { + QRect scrollBarRect = scrollbar->rect; + const uint64_t offset = 7; + scrollBarRect.setWidth(scrollBarRect.width() / 2); + scrollBarRect.setHeight(scrollBarRect.height() - 15); + int maxlen = ((scrollbar->orientation == Qt::Horizontal) ? + scrollBarRect.width() : scrollBarRect.height()); + int sliderlen; + + // calculate slider length + if (scrollbar->maximum != scrollbar->minimum) { + uint range = scrollbar->maximum - scrollbar->minimum; + sliderlen = (qint64(scrollbar->pageStep) * maxlen) / (range + scrollbar->pageStep); + + int slidermin = baseStyle()->pixelMetric(PM_ScrollBarSliderMin, scrollbar, widget); + if (sliderlen < slidermin || range > INT_MAX / 2) + sliderlen = slidermin; + if (sliderlen > maxlen) + sliderlen = maxlen; + } else { + sliderlen = maxlen; + } + + int sliderstart = sliderPositionFromValue(scrollbar->minimum, + scrollbar->maximum, + scrollbar->sliderPosition, + maxlen - sliderlen, + scrollbar->upsideDown); + + switch (subControl) { + /* + * case SC_ScrollBarSubLine: // top/left button + * if (scrollbar->orientation == Qt::Horizontal) { + * int buttonWidth = qMin(scrollBarRect.width() / 2, sbextent); + * rect.setRect(0, 0, buttonWidth, scrollBarRect.height()); + * } else { + * int buttonHeight = qMin(scrollBarRect.height() / 2, sbextent); + * rect.setRect(0, 0, scrollBarRect.width(), buttonHeight); + * } + * break; + * case SC_ScrollBarAddLine: // bottom/right button + * if (scrollbar->orientation == Qt::Horizontal) { + * int buttonWidth = qMin(scrollBarRect.width()/2, sbextent); + * rect.setRect(scrollBarRect.width() - buttonWidth, 0, buttonWidth, scrollBarRect.height()); + * } else { + * int buttonHeight = qMin(scrollBarRect.height()/2, sbextent); + * rect.setRect(0, scrollBarRect.height() - buttonHeight, scrollBarRect.width(), buttonHeight); + * } + * break; + */ + case SC_ScrollBarSubPage: // between top/left button and slider + if (scrollbar->orientation == Qt::Horizontal) + rect.setRect(0, 0, sliderstart, scrollBarRect.height()); + else + rect.setRect(0, 0, scrollBarRect.width(), sliderstart); + break; + case SC_ScrollBarAddPage: // between bottom/right button and slider + if (scrollbar->orientation == Qt::Horizontal) + rect.setRect(sliderstart + sliderlen, 0, + maxlen - sliderstart - sliderlen, scrollBarRect.height()); + else + rect.setRect(0, sliderstart + sliderlen, scrollBarRect.width(), + maxlen - sliderstart - sliderlen); + break; + case SC_ScrollBarGroove: + if (scrollbar->orientation == Qt::Horizontal) + rect.setRect(offset, 0, scrollBarRect.width(), + scrollBarRect.height()); + else + rect.setRect(0, offset, scrollBarRect.width(), + scrollBarRect.height() ); + break; + case SC_ScrollBarSlider: + if (scrollbar->orientation == Qt::Horizontal) + rect.setRect(sliderstart + offset, 0, sliderlen, scrollBarRect.height()); + else + rect.setRect(0, sliderstart + offset, scrollBarRect.width(), sliderlen); + break; + default: + break; + } + rect = visualRect(scrollbar->direction, scrollBarRect, rect); + return rect; + } + break; +#endif // QT_CONFIG(scrollbar) default: return baseStyle()->subControlRect(control, option, subControl, widget); break; @@ -97,6 +185,7 @@ public: void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, QPainter *painter, const QWidget *widget) const { + QColor outline; switch (cc) { #if QT_CONFIG(slider) case CC_MeterSlider: @@ -115,7 +204,7 @@ public: QPen oldPen = painter->pen(); QColor shadowAlpha(Qt::black); shadowAlpha.setAlpha(10); - QColor outline; + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) outline = highlightedOutline(option->palette); @@ -327,6 +416,213 @@ public: painter->setPen(oldPen); } break; + case CC_ScrollBar: + painter->save(); + if (const QStyleOptionSlider *scrollBar = qstyleoption_cast(option)) { + bool wasActive = false; + qreal expandScale = 1.0; + qreal expandOffset = -1.0; + QObject *styleObject = option->styleObject; + + QColor buttonColor = calculateButtonColor(option->palette); + QColor gradientStartColor = buttonColor.lighter(104); + QColor gradientStopColor = buttonColor.darker(102); + + bool transient = styleHint(SH_ScrollBar_Transient, option, widget); + bool horizontal = scrollBar->orientation == Qt::Horizontal; + bool sunken = scrollBar->state & State_Sunken; + + QRect scrollBarSubLine = subControlRect(cc, scrollBar, SC_ScrollBarSubLine, widget); + QRect scrollBarAddLine = subControlRect(cc, scrollBar, SC_ScrollBarAddLine, widget); + QRect scrollBarSlider = subControlRect(cc, scrollBar, SC_ScrollBarSlider, widget); + QRect scrollBarGroove = subControlRect(cc, scrollBar, SC_ScrollBarGroove, widget); + + QRect rect = option->rect; + outline = highlightedOutline(option->palette); + QColor alphaOutline = outline; + alphaOutline.setAlpha(180); + + QColor arrowColor = option->palette.windowText().color(); + //QColor arrowColor = QColor(188,143,143,100); + arrowColor.setAlpha(160); + + const QColor bgColor = backgroundColor(option->palette, widget); + const bool isDarkBg = bgColor.red() < 128 && bgColor.green() < 128 && bgColor.blue() < 128; + + if (transient) { + if (horizontal) { + rect.setY(rect.y() + 4.5 - expandOffset); + scrollBarSlider.setY(scrollBarSlider.y() + 4.5 - expandOffset); + scrollBarGroove.setY(scrollBarGroove.y() + 4.5 - expandOffset); + + rect.setHeight(rect.height() * expandScale); + scrollBarGroove.setHeight(scrollBarGroove.height() * expandScale); + } else { + rect.setX(rect.x() + 4.5 - expandOffset); + scrollBarSlider.setX(scrollBarSlider.x() + 4.5 - expandOffset); + scrollBarGroove.setX(scrollBarGroove.x() + 4.5 - expandOffset); + + rect.setWidth(rect.width() * expandScale); + scrollBarGroove.setWidth(scrollBarGroove.width() * expandScale); + } + } + + // Paint groove + if ((!transient || scrollBar->activeSubControls || wasActive) && scrollBar->subControls & SC_ScrollBarGroove) { + QLinearGradient gradient(rect.center().x(), rect.top(), + rect.center().x(), rect.bottom()); + if (!horizontal) + gradient = QLinearGradient(rect.left(), rect.center().y(), + rect.right(), rect.center().y()); + if (!transient || !isDarkBg) { + gradient.setColorAt(0, buttonColor.darker(107)); + gradient.setColorAt(0.1, buttonColor.darker(105)); + gradient.setColorAt(0.9, buttonColor.darker(105)); + gradient.setColorAt(1, buttonColor.darker(107)); + } else { + gradient.setColorAt(0, bgColor.lighter(157)); + gradient.setColorAt(0.1, bgColor.lighter(155)); + gradient.setColorAt(0.9, bgColor.lighter(155)); + gradient.setColorAt(1, bgColor.lighter(157)); + } + + painter->save(); + //painter->fillRect(rect, gradient); + painter->setPen(Qt::NoPen); + painter->setPen(alphaOutline); + + QColor subtleEdge = alphaOutline; + subtleEdge.setAlpha(40); + painter->setPen(subtleEdge); + //painter->setBrush(Qt::NoBrush); + painter->setOpacity(0.3); + painter->setBrush(Qt::gray); + painter->drawRoundedRect(scrollBarGroove, 5, 5); + //painter->drawRoundedRect(scrollBarGroove, 5, 5); + painter->restore(); + } + + QRect pixmapRect = scrollBarSlider; + QLinearGradient gradient(pixmapRect.center().x(), pixmapRect.top(), + pixmapRect.center().x(), pixmapRect.bottom()); + if (!horizontal) + gradient = QLinearGradient(pixmapRect.left(), pixmapRect.center().y(), + pixmapRect.right(), pixmapRect.center().y()); + + QLinearGradient highlightedGradient = gradient; + + QColor midColor2 = mergedColors(gradientStartColor, gradientStopColor, 40); + gradient.setColorAt(0, calculateButtonColor(option->palette).lighter(108)); + gradient.setColorAt(1, calculateButtonColor(option->palette)); + + highlightedGradient.setColorAt(0, gradientStartColor.darker(102)); + highlightedGradient.setColorAt(1, gradientStopColor.lighter(102)); + + // Paint slider + if (scrollBar->subControls & SC_ScrollBarSlider) { + if (transient) { + QRect rect = scrollBarSlider.adjusted(horizontal ? 1 : 2, horizontal ? 2 : 1, -1, -1); + painter->setPen(Qt::NoPen); + painter->setBrush(isDarkBg ? lightShade() : darkShade()); + int r = qMin(rect.width(), rect.height()) / 2; + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->drawRoundedRect(rect, r, r); + painter->restore(); + } else { + QRect pixmapRect = scrollBarSlider; + painter->setPen(QPen(alphaOutline)); + if (option->state & State_Sunken && scrollBar->activeSubControls & SC_ScrollBarSlider) + painter->setBrush(midColor2); + else if (option->state & State_MouseOver && scrollBar->activeSubControls & SC_ScrollBarSlider) + painter->setBrush(highlightedGradient); + else if (!isDarkBg) + painter->setBrush(gradient); + else + painter->setBrush(midColor2); + + painter->drawRoundedRect(pixmapRect.adjusted(horizontal ? -1 : 0, horizontal ? 0 : -1, horizontal ? 0 : -1, horizontal ? -1 : 0), 5, 5); + + painter->setPen(innerContrastLine()); + painter->drawRoundedRect(scrollBarSlider.adjusted(horizontal ? 0 : 1, horizontal ? 1 : 0, -1, -1), 5, 5); + + // Outer shadow + // painter->setPen(subtleEdge); + // if (horizontal) { + //// painter->drawLine(scrollBarSlider.topLeft() + QPoint(-2, 0), scrollBarSlider.bottomLeft() + QPoint(2, 0)); + //// painter->drawLine(scrollBarSlider.topRight() + QPoint(-2, 0), scrollBarSlider.bottomRight() + QPoint(2, 0)); + // } else { + //// painter->drawLine(pixmapRect.topLeft() + QPoint(0, -2), pixmapRect.bottomLeft() + QPoint(0, -2)); + //// painter->drawLine(pixmapRect.topRight() + QPoint(0, 2), pixmapRect.bottomRight() + QPoint(0, 2)); + // } + } + } + + /* + * // The SubLine (up/left) buttons + * if (!transient && scrollBar->subControls & SC_ScrollBarSubLine) { + * if ((scrollBar->activeSubControls & SC_ScrollBarSubLine) && sunken) + * painter->setBrush(gradientStopColor); + * else if ((scrollBar->activeSubControls & SC_ScrollBarSubLine)) + * painter->setBrush(highlightedGradient); + * else + * painter->setBrush(gradient); + * + * painter->setPen(Qt::NoPen); + * painter->drawRoundedRect(scrollBarSubLine.adjusted(horizontal ? 0 : 1, horizontal ? 1 : 0, 0, 0), 1, 1); + * painter->setPen(QPen(alphaOutline)); + * if (option->state & State_Horizontal) { + * painter->drawRect(scrollBarSubLine.adjusted(horizontal ? 0 : 1, 0, horizontal ? 1 : 0, horizontal ? -1 : 0)); + * } else { + * painter->drawRoundedRect(scrollBarSubLine.adjusted(0, 0, horizontal ? 0 : -1, 0), 1, 1); + * } + * + * QRect upRect = scrollBarSubLine.adjusted(horizontal ? 0 : 1, horizontal ? 1 : 0, horizontal ? -2 : -1, horizontal ? -1 : -2); + * painter->setBrush(Qt::NoBrush); + * painter->setPen(innerContrastLine()); + * painter->drawRect(upRect); + * + * // Arrows + * Qt::ArrowType arrowType = Qt::UpArrow; + * if (option->state & State_Horizontal) + * arrowType = option->direction == Qt::LeftToRight ? Qt::LeftArrow : Qt::RightArrow; + * qt_fusion_draw_arrow(arrowType, painter, option, upRect, arrowColor); + * } + * + * // The AddLine (down/right) button + * if (!transient && scrollBar->subControls & SC_ScrollBarAddLine) { + * if ((scrollBar->activeSubControls & SC_ScrollBarAddLine) && sunken) + * painter->setBrush(gradientStopColor); + * else if ((scrollBar->activeSubControls & SC_ScrollBarAddLine)) + * painter->setBrush(midColor2); + * else + * painter->setBrush(gradient); + * + * painter->setPen(Qt::NoPen); + * painter->drawRect(scrollBarAddLine.adjusted(horizontal ? 0 : 1, horizontal ? 1 : 0, 0, 0)); + * painter->setPen(QPen(alphaOutline, 1)); + * if (option->state & State_Horizontal) { + * painter->drawRect(scrollBarAddLine.adjusted(horizontal ? -1 : 0, 0, horizontal ? -1 : 0, horizontal ? -1 : 0)); + * } else { + * painter->drawRect(scrollBarAddLine.adjusted(0, horizontal ? 0 : -1, horizontal ? 0 : -1, horizontal ? 0 : -1)); + * } + * + * QRect downRect = scrollBarAddLine.adjusted(1, 1, -1, -1); + * painter->setPen(innerContrastLine()); + * painter->setBrush(Qt::NoBrush); + * painter->drawRect(downRect); + * + * Qt::ArrowType arrowType = Qt::DownArrow; + * if (option->state & State_Horizontal) + * arrowType = option->direction == Qt::LeftToRight ? Qt::RightArrow : Qt::LeftArrow; + * qt_fusion_draw_arrow(arrowType, painter, option, downRect, arrowColor); + * } + */ + + } + painter->restore(); + break; default: baseStyle()->drawComplexControl(cc, option, painter, widget); #endif // QT_CONFIG(slider) @@ -341,42 +637,127 @@ private: FromRight }; - - static QLinearGradient qt_fusion_gradient(const QRect &rect, const QBrush &baseColor, Direction direction = TopDown) + { + int x = rect.center().x(); + int y = rect.center().y(); + QLinearGradient gradient; + switch (direction) { + case FromLeft: + gradient = QLinearGradient(rect.left(), y, rect.right(), y); + break; + case FromRight: + gradient = QLinearGradient(rect.right(), y, rect.left(), y); + break; + case BottomUp: + gradient = QLinearGradient(x, rect.bottom(), x, rect.top()); + break; + case TopDown: + default: + gradient = QLinearGradient(x, rect.top(), x, rect.bottom()); + break; + } + if (baseColor.gradient()) + gradient.setStops(baseColor.gradient()->stops()); + else { + QColor gradientStartColor = baseColor.color().lighter(124); + QColor gradientStopColor = baseColor.color().lighter(102); + gradient.setColorAt(0, gradientStartColor); + gradient.setColorAt(1, gradientStopColor); + // Uncomment for adding shiny shading + // QColor midColor1 = mergedColors(gradientStartColor, gradientStopColor, 55); + // QColor midColor2 = mergedColors(gradientStartColor, gradientStopColor, 45); + // gradient.setColorAt(0.5, midColor1); + // gradient.setColorAt(0.501, midColor2); + } + return gradient; + } + + QColor calculateButtonColor(const QPalette &pal) const { + QColor buttonColor = pal.button().color(); + int val = qGray(buttonColor.rgb()); + buttonColor = buttonColor.lighter(100 + qMax(1, (180 - val)/6)); + buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, buttonColor.value()); + return buttonColor; + } + + // Used for grip handles + QColor lightShade() const { + return QColor(255, 255, 255, 90); + } + + QColor darkShade() const { + return QColor(0, 0, 0, 60); + } + + QColor innerContrastLine() const { + return QColor(255, 255, 255, 30); + } + + static QColor mergedColors(const QColor &colorA, const QColor &colorB, int factor = 50) { + const int maxFactor = 100; + QColor tmp = colorA; + tmp.setRed((tmp.red() * factor) / maxFactor + (colorB.red() * (maxFactor - factor)) / maxFactor); + tmp.setGreen((tmp.green() * factor) / maxFactor + (colorB.green() * (maxFactor - factor)) / maxFactor); + tmp.setBlue((tmp.blue() * factor) / maxFactor + (colorB.blue() * (maxFactor - factor)) / maxFactor); + return tmp; + } + + + static void qt_fusion_draw_arrow(Qt::ArrowType type, QPainter *painter, const QStyleOption *option, const QRect &rect, const QColor &color) { - int x = rect.center().x(); - int y = rect.center().y(); - QLinearGradient gradient; - switch (direction) { - case FromLeft: - gradient = QLinearGradient(rect.left(), y, rect.right(), y); - break; - case FromRight: - gradient = QLinearGradient(rect.right(), y, rect.left(), y); - break; - case BottomUp: - gradient = QLinearGradient(x, rect.bottom(), x, rect.top()); - break; - case TopDown: - default: - gradient = QLinearGradient(x, rect.top(), x, rect.bottom()); - break; + if (rect.isEmpty()) + return; + + const qreal dpi = calculateDpi(option); + const qreal dpr = painter->device()->devicePixelRatio(); + const int arrowWidth = int(dpiScaled(14, dpi)); + const int arrowHeight = int(dpiScaled(8, dpi)); + + const int arrowMax = qMin(arrowHeight, arrowWidth); + const int rectMax = qMin(rect.height(), rect.width()); + const int size = qMin(arrowMax, rectMax); + + QPixmap cachePixmap; + const QString cacheKey = "fusion-arrow"_L1 + QString::number(rect.size().width()) + QString::number(rect.size().height()) + QString::number(type); + + if (!QPixmapCache::find(cacheKey, &cachePixmap)) { + cachePixmap = styleCachePixmap(rect.size(), dpr); + QPainter cachePainter(&cachePixmap); + + QRectF arrowRect; + arrowRect.setWidth(size); + arrowRect.setHeight(arrowHeight * size / arrowWidth); + if (type == Qt::LeftArrow || type == Qt::RightArrow) + arrowRect = arrowRect.transposed(); + arrowRect.moveTo((rect.width() - arrowRect.width()) / 2.0, + (rect.height() - arrowRect.height()) / 2.0); + + std::array triangle; + switch (type) { + case Qt::DownArrow: + triangle = {arrowRect.topLeft(), arrowRect.topRight(), QPointF(arrowRect.center().x(), arrowRect.bottom())}; + break; + case Qt::RightArrow: + triangle = {arrowRect.topLeft(), arrowRect.bottomLeft(), QPointF(arrowRect.right(), arrowRect.center().y())}; + break; + case Qt::LeftArrow: + triangle = {arrowRect.topRight(), arrowRect.bottomRight(), QPointF(arrowRect.left(), arrowRect.center().y())}; + break; + default: + triangle = {arrowRect.bottomLeft(), arrowRect.bottomRight(), QPointF(arrowRect.center().x(), arrowRect.top())}; + break; + } + + cachePainter.setPen(Qt::NoPen); + cachePainter.setBrush(color); + cachePainter.setRenderHint(QPainter::Antialiasing); + cachePainter.drawPolygon(triangle.data(), int(triangle.size())); + + QPixmapCache::insert(cacheKey, cachePixmap); } - if (baseColor.gradient()) - gradient.setStops(baseColor.gradient()->stops()); - else { - QColor gradientStartColor = baseColor.color().lighter(124); - QColor gradientStopColor = baseColor.color().lighter(102); - gradient.setColorAt(0, gradientStartColor); - gradient.setColorAt(1, gradientStopColor); - // Uncomment for adding shiny shading - // QColor midColor1 = mergedColors(gradientStartColor, gradientStopColor, 55); - // QColor midColor2 = mergedColors(gradientStartColor, gradientStopColor, 45); - // gradient.setColorAt(0.5, midColor1); - // gradient.setColorAt(0.501, midColor2); - } - return gradient; + + painter->drawPixmap(rect, cachePixmap); } }; From 544da49e32f98e3f76d673d1e72540a02a0481c5 Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 6 Jun 2024 20:08:36 +0200 Subject: [PATCH 08/38] test: fixed unnamed sessions #6 --- src/back/backlasses.cpp | 2 +- src/back/backsessionclasses.cpp | 113 ++++++++++++++++++++++++-------- src/back/msinclude.h | 2 + src/qt/qtclasses.cpp | 2 +- src/qt/qtcommon.h | 2 +- 5 files changed, 91 insertions(+), 30 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index bfc0e83..60e6a93 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -475,7 +475,7 @@ void Endpoint::setFlow() { if(FAILED(this->endpoint->QueryInterface(__uuidof(IMMEndpoint), (void**)&flowGetter))) { log_debugcpp("no flow..."); } EDataFlow MSflow; - HRESULT vafllar = flowGetter->GetDataFlow(&MSflow); + flowGetter->GetDataFlow(&MSflow); this->flow = (MSflow == EDataFlow::eRender ? Flows::FLOW_PLAYBACK : Flows::FLOW_CAPTURE); log_debugcpp("Endpoint flow: " + std::to_string(flow)); flowGetter->Release(); diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index 80eeac8..ba0eaa6 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -196,11 +196,14 @@ std::wstring Session::fetchProcessName(DWORD pid) { /* * https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot * https://stackoverflow.com/questions/11843368/how-to-get-process-description + * https://notes.indezine.com/2018/05/microsoft-locale-ids.html#:~:text=Wait%2C%201033%20is%20the%20decimal,ID%20for%20English%20%E2%80%93%20United%20States. + * https://stackoverflow.com/questions/64321036/c-win32-getting-app-name-using-pid-and-executable-path */ /* Executable path retrieval */ std::wstring exePath = L""; - + std::wstring msixName; + HANDLE processList = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); if (processList == INVALID_HANDLE_VALUE) { log_wdebugcpp(L"aye no procname."); @@ -224,55 +227,111 @@ std::wstring Session::fetchProcessName(DWORD pid) { } CloseHandle(processList); - /* File description retrieval */ + /* File description retrieval: size and available lang-codepages */ struct LANGANDCODEPAGE { WORD wLanguage; WORD wCodePage; } *translationArray; DWORD filler; - DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(exePath.c_str(), &filler); + DWORD fileVersionInfoSize = GetFileVersionInfoSizeExW + (FILE_VER_GET_LOCALISED | FILE_VER_GET_NEUTRAL, exePath.c_str(), &filler); if (!fileVersionInfoSize) return exePath; void* fileVersionInfo = malloc(fileVersionInfoSize); - if(!GetFileVersionInfoW(exePath.c_str(),0,fileVersionInfoSize, fileVersionInfo)) + if(!GetFileVersionInfoExW(FILE_VER_GET_LOCALISED | FILE_VER_GET_NEUTRAL, + exePath.c_str(),0,fileVersionInfoSize, fileVersionInfo)) return exePath; UINT translationArrayLen = 0; if (!VerQueryValueW(fileVersionInfo, L"\\VarFileInfo\\Translation", (LPVOID*)&translationArray, &translationArrayLen)) return exePath; + + //File descriptor parsing + //TODO: https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserpreferreduilanguages + /* It is possible to retrieve user languages and try to use one of those before falling back to whatever + * is available. Also possible to hardcode en-US or any other lang-codepage combo. When an actual translation + * sysem is put in place, I'll come finish this up. + */ + uint64_t availableLangs = (translationArrayLen / sizeof(LANGANDCODEPAGE)); + if (!availableLangs) return exePath; - bool match = false; - for (UINT i = 0; i < (translationArrayLen / sizeof(LANGANDCODEPAGE)); i++) { - wchar_t fileDescriptionKey[256]; + int8_t syslangIdx = -1; + wchar_t metadataStringKey[256]; + wchar_t* metadataString = NULL; + //std::wstring name; + for (UINT i = 0; i < availableLangs; i++) { LANGID defaultUILanguage = GetUserDefaultUILanguage(); if (defaultUILanguage != translationArray[i].wLanguage) continue; - match = true; - wchar_t* fileDescription = NULL; - UINT fileDescriptionSize = 0; - swprintf(fileDescriptionKey, L"\\StringFileInfo\\%04x%04x\\FileDescription", - translationArray[i].wLanguage, translationArray[i].wCodePage); - if (VerQueryValueW(fileVersionInfo, fileDescriptionKey, (LPVOID*)&fileDescription, &fileDescriptionSize)) { - exePath = std::wstring(fileDescription); - } + syslangIdx = i; + break; } - if (!match && 1 <= (translationArrayLen / sizeof(LANGANDCODEPAGE))) { - wchar_t fileDescriptionKey[256]; + UINT metadataStringSize = 0; + swprintf(metadataStringKey, L"\\StringFileInfo\\%04x%04x\\FileDescription", + translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wLanguage, + translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wCodePage); + if (VerQueryValueW(fileVersionInfo, metadataStringKey, (LPVOID*)&metadataString, &metadataStringSize) + && metadataString[0] != '\0') { + return std::wstring(metadataString); + } + swprintf(metadataStringKey, L"\\StringFileInfo\\%04x%04x\\ProductName", + translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wLanguage, + translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wCodePage); + if (VerQueryValueW(fileVersionInfo, metadataStringKey, (LPVOID*)&metadataString, &metadataStringSize) + && metadataString[0] != '\0') { + return std::wstring(metadataString); + } -wchar_t* fileDescription = NULL; - UINT fileDescriptionSize = 0; - swprintf(fileDescriptionKey, L"\\StringFileInfo\\%04x%04x\\FileDescription", - translationArray[0].wLanguage, translationArray[0].wCodePage); - if (VerQueryValueW(fileVersionInfo, fileDescriptionKey, (LPVOID*)&fileDescription, &fileDescriptionSize)) { - exePath = std::wstring(fileDescription); - } - } + //MSIX? + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if(!process) return exePath; - free(fileVersionInfo); - return exePath; + //constant missing in mingw64. TB removed when I upgrade to a mingw64 ver that has it + #define APPLICATION_USER_MODEL_ID_MAX_LENGTH 130 + uint32_t length = APPLICATION_USER_MODEL_ID_MAX_LENGTH; + PWSTR userModelId = (PWSTR)malloc(length * sizeof(wchar_t)); + if(GetApplicationUserModelId(process, &length, userModelId) != ERROR_SUCCESS) { + CloseHandle(process); + return exePath; + } + CloseHandle(process); + + static constexpr wchar_t* prefix = L"shell:appsfolder\\"; + uint32_t prefixLen = wcslen(prefix); + uint32_t userModelIdLen = wcslen(userModelId); + wchar_t* fullName; + fullName = (prefixLen + userModelIdLen < length) + ? (wchar_t*)malloc(length * sizeof(wchar_t)) + : (wchar_t*)malloc((length * 2) * sizeof(wchar_t)); + for (int32_t i = prefixLen - 1; i >= 0; i--) { + fullName[i] = prefix[i]; + } + for (uint32_t i = 0; i < userModelIdLen + 1; i++) { + fullName[prefixLen + i] = userModelId[i]; + } + + IShellItem* si; + HRESULT hr = SHCreateItemFromParsingName(fullName, + nullptr, + IID_IShellItem, + (void**)&si + ); + free(fullName); + LPWSTR humanName = nullptr; + si->GetDisplayName(SIGDN_NORMALDISPLAY, &humanName); + if(humanName && humanName[0] != '\0') { + msixName = std::wstring(humanName); + CoTaskMemFree(humanName); + } + if(si) si->Release(); + + if (msixName.length() > 0) + return msixName; + else return exePath; + //free(fileVersionInfo); } //todo: conflicting names. change callback name diff --git a/src/back/msinclude.h b/src/back/msinclude.h index 16216f6..6f21ae1 100644 --- a/src/back/msinclude.h +++ b/src/back/msinclude.h @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index d6b4c48..48b248c 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -263,7 +263,7 @@ void MainWindow::compose() { log_to_file("dpr: %f \n", dpr); for (auto *epw : ews) { if (!epw) continue; - epw->calculateSize(windowWidth * dpr,screenHeight * screen->devicePixelRatio()); + epw->calculateSize(windowWidth, screenHeight); log_debugcpp("epw loop"); log_debugcpp("epw roles: " + print_as_binary((epw->getEndpointHandler()->getRoles()))); //std::bitset content = diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h index dc2797b..414e236 100644 --- a/src/qt/qtcommon.h +++ b/src/qt/qtcommon.h @@ -86,7 +86,7 @@ namespace StylingHelper { #endif } - static inline qreal calculateDpi(const QScreen screen) { + static inline qreal calculateDpi() { return QFontMetrics(QApplication::font()).fontDpi(); } From 42b30b1bf8622ddb06979d669f1b43e25377c312 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 6 Aug 2024 09:45:25 +0200 Subject: [PATCH 09/38] Og & wip: names --- qtest.pro | 2 +- src/back/backsessionclasses.cpp | 236 ++++++++++++++++++++++++++++---- src/back/backsessionclasses.h | 10 +- src/global.h | 2 + src/qt/qtclasses.cpp | 5 +- 5 files changed, 227 insertions(+), 28 deletions(-) diff --git a/qtest.pro b/qtest.pro index 303f059..b85fa53 100644 --- a/qtest.pro +++ b/qtest.pro @@ -1,4 +1,4 @@ -QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview +QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview -Og QMAKE_LFLAGS += --target=x86_64-w64-mingw32 -g -Wl,-pdb= -v LIBS += -LC:/capybara/libclang/x86_64-w64-mingw32/lib -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys -static -stdlib=libc++ -lunwind #"kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index ba0eaa6..41bf733 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -126,9 +126,12 @@ Session::Session(Endpoint* ep, IAudioSessionControl2* sessionControl, size_t idx else { LPWSTR sessionDisplayName; this->sessionControl->GetDisplayName(&sessionDisplayName); - if (!wcscmp(sessionDisplayName, L"")) - this->sessionName = this->fetchProcessName(pid); - else + if (!wcscmp(sessionDisplayName, L"")) { + std::wstring exePath; + if (getExePath(pid, &exePath)) + fetchName(exePath, pid); + else this->sessionName = std::wstring(LSTRING_UNNAMED_SESSION); + } else this->sessionName = std::wstring(sessionDisplayName); CoTaskMemFree(sessionDisplayName); } @@ -192,22 +195,20 @@ void Session::setMute(NGuid guid, bool muted) { if(FAILED(sessionVolume->SetMute(muted, &tempMsGuid))) { log_wdebugcpp(std::wstring(L"SessionVolume null?")); }; } -std::wstring Session::fetchProcessName(DWORD pid) { +bool Session::getExePath(DWORD pid, std::wstring *exePath) { /* * https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot * https://stackoverflow.com/questions/11843368/how-to-get-process-description * https://notes.indezine.com/2018/05/microsoft-locale-ids.html#:~:text=Wait%2C%201033%20is%20the%20decimal,ID%20for%20English%20%E2%80%93%20United%20States. * https://stackoverflow.com/questions/64321036/c-win32-getting-app-name-using-pid-and-executable-path */ - - /* Executable path retrieval */ - std::wstring exePath = L""; - std::wstring msixName; + + //std::wstring msixName; HANDLE processList = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); if (processList == INVALID_HANDLE_VALUE) { log_wdebugcpp(L"aye no procname."); - return exePath; + return false; } MODULEENTRY32W me32w; @@ -215,7 +216,7 @@ std::wstring Session::fetchProcessName(DWORD pid) { if(Module32FirstW(processList, &me32w)) { do { if (me32w.th32ProcessID == pid) { - exePath = std::wstring(me32w.szExePath); + *exePath = std::wstring(me32w.szExePath); break; /* * However, if the calling process is a 32-bit process, you must call the @@ -226,7 +227,10 @@ std::wstring Session::fetchProcessName(DWORD pid) { } while(Module32NextW(processList, &me32w)); } CloseHandle(processList); + return true; +} +bool Session::fetchNameViaFD(std::wstring exePath, DWORD pid, std::wstring *sessionName) { /* File description retrieval: size and available lang-codepages */ struct LANGANDCODEPAGE { WORD wLanguage; @@ -236,16 +240,16 @@ std::wstring Session::fetchProcessName(DWORD pid) { DWORD filler; DWORD fileVersionInfoSize = GetFileVersionInfoSizeExW (FILE_VER_GET_LOCALISED | FILE_VER_GET_NEUTRAL, exePath.c_str(), &filler); - if (!fileVersionInfoSize) return exePath; + if (!fileVersionInfoSize) return false; void* fileVersionInfo = malloc(fileVersionInfoSize); if(!GetFileVersionInfoExW(FILE_VER_GET_LOCALISED | FILE_VER_GET_NEUTRAL, exePath.c_str(),0,fileVersionInfoSize, fileVersionInfo)) - return exePath; + return false; UINT translationArrayLen = 0; if (!VerQueryValueW(fileVersionInfo, L"\\VarFileInfo\\Translation", (LPVOID*)&translationArray, &translationArrayLen)) - return exePath; + return false; //File descriptor parsing //TODO: https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserpreferreduilanguages @@ -254,7 +258,7 @@ std::wstring Session::fetchProcessName(DWORD pid) { * sysem is put in place, I'll come finish this up. */ uint64_t availableLangs = (translationArrayLen / sizeof(LANGANDCODEPAGE)); - if (!availableLangs) return exePath; + if (!availableLangs) { free(fileVersionInfo); return false; } int8_t syslangIdx = -1; wchar_t metadataStringKey[256]; @@ -275,19 +279,30 @@ std::wstring Session::fetchProcessName(DWORD pid) { translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wCodePage); if (VerQueryValueW(fileVersionInfo, metadataStringKey, (LPVOID*)&metadataString, &metadataStringSize) && metadataString[0] != '\0') { - return std::wstring(metadataString); + free(fileVersionInfo); + *sessionName = std::wstring(metadataString); + return true; } swprintf(metadataStringKey, L"\\StringFileInfo\\%04x%04x\\ProductName", translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wLanguage, translationArray[(syslangIdx < 0 ? 0 : syslangIdx)].wCodePage); if (VerQueryValueW(fileVersionInfo, metadataStringKey, (LPVOID*)&metadataString, &metadataStringSize) && metadataString[0] != '\0') { - return std::wstring(metadataString); + free(fileVersionInfo); + *sessionName = std::wstring(metadataString); + return true; } - //MSIX? + if(fileVersionInfo) + free(fileVersionInfo); + + return false; +} + + +bool Session::fetchNameViaMSIX(std::wstring exePath, DWORD pid, std::wstring *sessionName) { HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - if(!process) return exePath; + if(!process) return false; //constant missing in mingw64. TB removed when I upgrade to a mingw64 ver that has it #define APPLICATION_USER_MODEL_ID_MAX_LENGTH 130 @@ -295,7 +310,7 @@ std::wstring Session::fetchProcessName(DWORD pid) { PWSTR userModelId = (PWSTR)malloc(length * sizeof(wchar_t)); if(GetApplicationUserModelId(process, &length, userModelId) != ERROR_SUCCESS) { CloseHandle(process); - return exePath; + return false; } CloseHandle(process); @@ -323,17 +338,190 @@ std::wstring Session::fetchProcessName(DWORD pid) { LPWSTR humanName = nullptr; si->GetDisplayName(SIGDN_NORMALDISPLAY, &humanName); if(humanName && humanName[0] != '\0') { - msixName = std::wstring(humanName); + *sessionName = std::wstring(humanName); CoTaskMemFree(humanName); } if(si) si->Release(); - if (msixName.length() > 0) - return msixName; - else return exePath; - //free(fileVersionInfo); + if (sessionName->length() > 0) + return true; + else return false; + + } +BOOL test(HWND hwnd, LPARAM lParam) { + int length = GetWindowTextLength(hwnd); + wchar_t* buffer = new wchar_t[length + 1]; + GetWindowTextW(hwnd, buffer, length + 1); + std::wstring windowTitle(buffer); + delete[] buffer; + + // List visible windows with a non-empty title + if (IsWindowVisible(hwnd) && length != 0) { + //std::wcout << hwnd << ": " << windowTitle << std::endl; + log_wdebugcpp(windowTitle); + return FALSE; + } + return TRUE; + + /* + * auto pParams = (DWORD)(lParam); + * + * DWORD processId; + * if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams) { + * // Stop enumerating + * //SetLastError(-1); + * //pParams->first = hwnd; + * //return FALSE; + * } + * + * // Continue enumerating + * return TRUE; + */ +} + + +bool Session::fetchNameViaWindowName(std::wstring exePath, DWORD pid, std::wstring *sessionName) { + std::pair params = { 0, pid }; + + BOOL result = EnumWindows(test, NULL); + + /* + * if (!params.first) goto msix; + */ + + + /* + * if (!result && GetLastError() == -1 && params.first) { + * return params.first; + * } + */ +} + +void Session::fetchName(std::wstring exePath, DWORD pid) { + if(fetchNameViaWindowName(exePath, pid, &this->sessionName)) + return; + else if(fetchNameViaMSIX(exePath, pid, &this->sessionName)) + return; + else if(!fetchNameViaFD(exePath, pid, &this->sessionName)) + this->sessionName = exePath; + + /* + * std::thread ttest(&Session::fetchNameViaWindowName, this, exePath, pid, &this->sessionName); + * ttest.join(); + */ +} + +/* + * std::wstring Session::fetchProcessName(DWORD pid) { + * /\* Executable path retrieval *\/ + * std::wstring exePath = L""; + * std::wstring msixName; + * + * HANDLE processList = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); + * if (processList == INVALID_HANDLE_VALUE) { + * log_wdebugcpp(L"aye no procname."); + * return exePath; + * } + * + * MODULEENTRY32W me32w; + * me32w.dwSize = sizeof(MODULEENTRY32W); + * if(Module32FirstW(processList, &me32w)) { + * do { + * if (me32w.th32ProcessID == pid) { + * exePath = std::wstring(me32w.szExePath); + * break; + * /\* + * * However, if the calling process is a 32-bit process, you must call the + * * QueryFullProcessImageName function to retrieve the full path of the + * * executable file for a 64-bit process. + * *\/ + * } + * } while(Module32NextW(processList, &me32w)); + * } + * CloseHandle(processList); + * + * + * //No FD info available. Window name? + * nofdinfo: + * if(fileVersionInfo) + * free(fileVersionInfo); + * + * std::pair params = { 0, pid }; + * BOOL result = EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL { + * auto pParams = (std::pair*)(lParam); + * + * DWORD processId; + * if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { + * // Stop enumerating + * SetLastError(-1); + * pParams->first = hwnd; + * return FALSE; + * } + * + * // Continue enumerating + * return TRUE; + * }, (LPARAM)¶ms); + * + * if (!params.first) goto msix; + * + * + * /\* + * * if (!result && GetLastError() == -1 && params.first) { + * * return params.first; + * * } + * *\/ + * + * //No window info. MSIX? + * HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + * if(!process) return exePath; + * + * //constant missing in mingw64. TB removed when I upgrade to a mingw64 ver that has it + * #define APPLICATION_USER_MODEL_ID_MAX_LENGTH 130 + * uint32_t length = APPLICATION_USER_MODEL_ID_MAX_LENGTH; + * PWSTR userModelId = (PWSTR)malloc(length * sizeof(wchar_t)); + * if(GetApplicationUserModelId(process, &length, userModelId) != ERROR_SUCCESS) { + * CloseHandle(process); + * return exePath; + * } + * CloseHandle(process); + * + * static constexpr wchar_t* prefix = L"shell:appsfolder\\"; + * uint32_t prefixLen = wcslen(prefix); + * uint32_t userModelIdLen = wcslen(userModelId); + * wchar_t* fullName; + * fullName = (prefixLen + userModelIdLen < length) + * ? (wchar_t*)malloc(length * sizeof(wchar_t)) + * : (wchar_t*)malloc((length * 2) * sizeof(wchar_t)); + * for (int32_t i = prefixLen - 1; i >= 0; i--) { + * fullName[i] = prefix[i]; + * } + * for (uint32_t i = 0; i < userModelIdLen + 1; i++) { + * fullName[prefixLen + i] = userModelId[i]; + * } + * + * IShellItem* si; + * HRESULT hr = SHCreateItemFromParsingName(fullName, + * nullptr, + * IID_IShellItem, + * (void**)&si + * ); + * free(fullName); + * LPWSTR humanName = nullptr; + * si->GetDisplayName(SIGDN_NORMALDISPLAY, &humanName); + * if(humanName && humanName[0] != '\0') { + * msixName = std::wstring(humanName); + * CoTaskMemFree(humanName); + * } + * if(si) si->Release(); + * + * if (msixName.length() > 0) + * return msixName; + * else return exePath; + * } + */ + //todo: conflicting names. change callback name void Session::setState(SessionState state) { sessionState = state; diff --git a/src/back/backsessionclasses.h b/src/back/backsessionclasses.h index 2b5db99..9e0f7a8 100644 --- a/src/back/backsessionclasses.h +++ b/src/back/backsessionclasses.h @@ -4,6 +4,7 @@ #include "global.h" #include "contclasses.h" +#include class Endpoint; class SessionStateCallback : public IAudioSessionEvents { @@ -47,13 +48,18 @@ class Session { //uint32_t getChannelCount(); private: - std::wstring fetchProcessName(DWORD pid); + bool getExePath(DWORD pid, /*out*/ std::wstring *exePath); + void fetchName(std::wstring exePath, DWORD pid); + bool fetchNameViaFD(std::wstring exePath, DWORD pid, /*out*/ std::wstring *sessionName); + bool fetchNameViaMSIX(std::wstring exePath, DWORD pid, /*out*/ std::wstring *sessionName); + bool fetchNameViaWindowName(std::wstring exePath, DWORD pid, /*out*/ std::wstring *sessionName); + /* std::wstring fetchProcessName(DWORD pid); */ std::wstring sessionName; SessionState sessionState; Endpoint* ep; IAudioSessionControl2* sessionControl = nullptr; IAudioMeterInformation* meterInformation = nullptr; ISimpleAudioVolume* sessionVolume = nullptr; - size_t idx; + size_t idx; }; diff --git a/src/global.h b/src/global.h index b8b9bcb..5d327dc 100644 --- a/src/global.h +++ b/src/global.h @@ -30,6 +30,8 @@ #define STRING_CP "Open Control Panel" #define STRING_ABOUT "About" #define STRING_STARTUP "Run at startup" + +#define LSTRING_UNNAMED_SESSION L"Unnamed session" //INIT BACK enum AudioChannel { diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 48b248c..cbe8106 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -326,6 +326,7 @@ SessionWidget::SessionWidget(uint64_t idx, SessionHandler* sh, QWidget *parent) muteButton = new QCheckBox(this); mainLabel = new QLabel(QString::fromStdWString(sh->getName()), this); + mainLabel->setToolTip(QString::fromStdWString(sh->getName())); mainSlider = new MeterSlider(Qt::Horizontal, this); //mainLabel->setMaximumWidth(150 /*1/16ish 1080p*/); @@ -368,8 +369,10 @@ SessionWidget::SessionWidget(uint64_t idx, SessionHandler* sh, QWidget *parent) connect(volumePoller, &QTimer::timeout, [this, sh](){ //if (memcmp(osh->callbackInfo[idx]->caller, osh->getGuid(), sizeof(NGuid)) == 0) return; CHECK IF THIS PROGRAM GENERATED THE FUNSIES IS NO LONGER IN USE FOR NOW. //todo: global + constexpr + ratio - if (sh->getVolumeInfo()->isNameChanged) + if (sh->getVolumeInfo()->isNameChanged) { mainLabel->setText(QString::fromStdWString(sh->getName())); + mainLabel->setToolTip(QString::fromStdWString(sh->getName())); + } const float roundingFactor = 0.005; mainSlider->blockSignals(true); muteButton->blockSignals(true); From 517f117575b57c4fa360efa70dce06c8f133454a Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 6 Aug 2024 14:49:52 +0200 Subject: [PATCH 10/38] o0 & windownames --- qtest.pro | 2 +- src/back/backsessionclasses.cpp | 184 +++++--------------------------- 2 files changed, 27 insertions(+), 159 deletions(-) diff --git a/qtest.pro b/qtest.pro index b85fa53..cc46052 100644 --- a/qtest.pro +++ b/qtest.pro @@ -1,4 +1,4 @@ -QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview -Og +QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview -O0 -Werror=return-type QMAKE_LFLAGS += --target=x86_64-w64-mingw32 -g -Wl,-pdb= -v LIBS += -LC:/capybara/libclang/x86_64-w64-mingw32/lib -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys -static -stdlib=libc++ -lunwind #"kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index 41bf733..40352bf 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -350,178 +350,46 @@ bool Session::fetchNameViaMSIX(std::wstring exePath, DWORD pid, std::wstring *se } -BOOL test(HWND hwnd, LPARAM lParam) { - int length = GetWindowTextLength(hwnd); - wchar_t* buffer = new wchar_t[length + 1]; - GetWindowTextW(hwnd, buffer, length + 1); - std::wstring windowTitle(buffer); - delete[] buffer; - - // List visible windows with a non-empty title - if (IsWindowVisible(hwnd) && length != 0) { - //std::wcout << hwnd << ": " << windowTitle << std::endl; - log_wdebugcpp(windowTitle); - return FALSE; - } - return TRUE; - - /* - * auto pParams = (DWORD)(lParam); - * - * DWORD processId; - * if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams) { - * // Stop enumerating - * //SetLastError(-1); - * //pParams->first = hwnd; - * //return FALSE; - * } - * - * // Continue enumerating - * return TRUE; - */ -} - bool Session::fetchNameViaWindowName(std::wstring exePath, DWORD pid, std::wstring *sessionName) { + //lParam is documented as in, so... Beware of future explosions, ig? std::pair params = { 0, pid }; - BOOL result = EnumWindows(test, NULL); - - /* - * if (!params.first) goto msix; - */ - + BOOL result = EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL { + auto pParams = (std::pair*)(lParam); - /* - * if (!result && GetLastError() == -1 && params.first) { - * return params.first; - * } - */ + DWORD processId; + if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { + // Stop enumerating + SetLastError(-1); + pParams->first = hwnd; + return FALSE; + } + + // Continue enumerating + return TRUE; + } , (LPARAM)¶ms); + + if(!result && GetLastError() == -1 && params.first) { + int length = GetWindowTextLength(params.first); + wchar_t* buffer = new wchar_t[length + 1]; + GetWindowTextW(params.first, buffer, length + 1); + *sessionName = buffer; + delete[] buffer; + return true; + } + return false; } void Session::fetchName(std::wstring exePath, DWORD pid) { - if(fetchNameViaWindowName(exePath, pid, &this->sessionName)) + if(fetchNameViaFD(exePath, pid, &this->sessionName)) return; else if(fetchNameViaMSIX(exePath, pid, &this->sessionName)) return; - else if(!fetchNameViaFD(exePath, pid, &this->sessionName)) + else if(!fetchNameViaWindowName(exePath, pid, &this->sessionName)) this->sessionName = exePath; - - /* - * std::thread ttest(&Session::fetchNameViaWindowName, this, exePath, pid, &this->sessionName); - * ttest.join(); - */ } -/* - * std::wstring Session::fetchProcessName(DWORD pid) { - * /\* Executable path retrieval *\/ - * std::wstring exePath = L""; - * std::wstring msixName; - * - * HANDLE processList = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); - * if (processList == INVALID_HANDLE_VALUE) { - * log_wdebugcpp(L"aye no procname."); - * return exePath; - * } - * - * MODULEENTRY32W me32w; - * me32w.dwSize = sizeof(MODULEENTRY32W); - * if(Module32FirstW(processList, &me32w)) { - * do { - * if (me32w.th32ProcessID == pid) { - * exePath = std::wstring(me32w.szExePath); - * break; - * /\* - * * However, if the calling process is a 32-bit process, you must call the - * * QueryFullProcessImageName function to retrieve the full path of the - * * executable file for a 64-bit process. - * *\/ - * } - * } while(Module32NextW(processList, &me32w)); - * } - * CloseHandle(processList); - * - * - * //No FD info available. Window name? - * nofdinfo: - * if(fileVersionInfo) - * free(fileVersionInfo); - * - * std::pair params = { 0, pid }; - * BOOL result = EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL { - * auto pParams = (std::pair*)(lParam); - * - * DWORD processId; - * if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { - * // Stop enumerating - * SetLastError(-1); - * pParams->first = hwnd; - * return FALSE; - * } - * - * // Continue enumerating - * return TRUE; - * }, (LPARAM)¶ms); - * - * if (!params.first) goto msix; - * - * - * /\* - * * if (!result && GetLastError() == -1 && params.first) { - * * return params.first; - * * } - * *\/ - * - * //No window info. MSIX? - * HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - * if(!process) return exePath; - * - * //constant missing in mingw64. TB removed when I upgrade to a mingw64 ver that has it - * #define APPLICATION_USER_MODEL_ID_MAX_LENGTH 130 - * uint32_t length = APPLICATION_USER_MODEL_ID_MAX_LENGTH; - * PWSTR userModelId = (PWSTR)malloc(length * sizeof(wchar_t)); - * if(GetApplicationUserModelId(process, &length, userModelId) != ERROR_SUCCESS) { - * CloseHandle(process); - * return exePath; - * } - * CloseHandle(process); - * - * static constexpr wchar_t* prefix = L"shell:appsfolder\\"; - * uint32_t prefixLen = wcslen(prefix); - * uint32_t userModelIdLen = wcslen(userModelId); - * wchar_t* fullName; - * fullName = (prefixLen + userModelIdLen < length) - * ? (wchar_t*)malloc(length * sizeof(wchar_t)) - * : (wchar_t*)malloc((length * 2) * sizeof(wchar_t)); - * for (int32_t i = prefixLen - 1; i >= 0; i--) { - * fullName[i] = prefix[i]; - * } - * for (uint32_t i = 0; i < userModelIdLen + 1; i++) { - * fullName[prefixLen + i] = userModelId[i]; - * } - * - * IShellItem* si; - * HRESULT hr = SHCreateItemFromParsingName(fullName, - * nullptr, - * IID_IShellItem, - * (void**)&si - * ); - * free(fullName); - * LPWSTR humanName = nullptr; - * si->GetDisplayName(SIGDN_NORMALDISPLAY, &humanName); - * if(humanName && humanName[0] != '\0') { - * msixName = std::wstring(humanName); - * CoTaskMemFree(humanName); - * } - * if(si) si->Release(); - * - * if (msixName.length() > 0) - * return msixName; - * else return exePath; - * } - */ - //todo: conflicting names. change callback name void Session::setState(SessionState state) { sessionState = state; From e42dbaa19438780980f813e5b0cca504b4764b27 Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 8 Aug 2024 15:04:17 +0200 Subject: [PATCH 11/38] session shown name parsing, pending refactor --- src/back/backsessionclasses.cpp | 41 +++++++++++++++++++++++---------- src/back/backsessionclasses.h | 4 ++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index 40352bf..1bfc37f 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -128,11 +128,14 @@ Session::Session(Endpoint* ep, IAudioSessionControl2* sessionControl, size_t idx this->sessionControl->GetDisplayName(&sessionDisplayName); if (!wcscmp(sessionDisplayName, L"")) { std::wstring exePath; - if (getExePath(pid, &exePath)) - fetchName(exePath, pid); - else this->sessionName = std::wstring(LSTRING_UNNAMED_SESSION); + if (getExePath(pid, &exePath)) { + if (fetchName(exePath, pid)) goto nameFound; + } + if (fetchNameViaWindowName(pid, &this->sessionName)) goto nameFound; } else this->sessionName = std::wstring(sessionDisplayName); + + nameFound: CoTaskMemFree(sessionDisplayName); } } @@ -207,7 +210,7 @@ bool Session::getExePath(DWORD pid, std::wstring *exePath) { HANDLE processList = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); if (processList == INVALID_HANDLE_VALUE) { - log_wdebugcpp(L"aye no procname."); + log_wdebugcpp(L"aye no procname. -> " + std::to_wstring(GetLastError())); return false; } @@ -351,7 +354,7 @@ bool Session::fetchNameViaMSIX(std::wstring exePath, DWORD pid, std::wstring *se } -bool Session::fetchNameViaWindowName(std::wstring exePath, DWORD pid, std::wstring *sessionName) { +bool Session::fetchNameViaWindowName(DWORD pid, std::wstring *sessionName) { //lParam is documented as in, so... Beware of future explosions, ig? std::pair params = { 0, pid }; @@ -359,7 +362,11 @@ bool Session::fetchNameViaWindowName(std::wstring exePath, DWORD pid, std::wstri auto pParams = (std::pair*)(lParam); DWORD processId; - if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { + //&& GetWindow(hwnd, GW_OWNER) == 0 + if (IsWindowVisible(hwnd) && GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { + int length = GetWindowTextLength(hwnd); + if (!length) return TRUE; + // Stop enumerating SetLastError(-1); pParams->first = hwnd; @@ -371,6 +378,7 @@ bool Session::fetchNameViaWindowName(std::wstring exePath, DWORD pid, std::wstri } , (LPARAM)¶ms); if(!result && GetLastError() == -1 && params.first) { + //todo: double-dipping length... Not a fan int length = GetWindowTextLength(params.first); wchar_t* buffer = new wchar_t[length + 1]; GetWindowTextW(params.first, buffer, length + 1); @@ -381,13 +389,22 @@ bool Session::fetchNameViaWindowName(std::wstring exePath, DWORD pid, std::wstri return false; } -void Session::fetchName(std::wstring exePath, DWORD pid) { +bool Session::fetchName(std::wstring exePath, DWORD pid) { + /* + * if(fetchNameViaWindowName(exePath, pid, &this->sessionName)) + * return; + * else if(fetchNameViaMSIX(exePath, pid, &this->sessionName)) + * return; + * else if(!fetchNameViaFD(exePath, pid, &this->sessionName)) + * this->sessionName = exePath; + */ + if(fetchNameViaFD(exePath, pid, &this->sessionName)) - return; - else if(fetchNameViaMSIX(exePath, pid, &this->sessionName)) - return; - else if(!fetchNameViaWindowName(exePath, pid, &this->sessionName)) - this->sessionName = exePath; + return true; + return fetchNameViaMSIX(exePath, pid, &this->sessionName); + //else if(!fetchNameViaWindowName(exePath, pid, &this->sessionName)) + /// this->sessionName = exePath; + } //todo: conflicting names. change callback name diff --git a/src/back/backsessionclasses.h b/src/back/backsessionclasses.h index 9e0f7a8..3f73f3d 100644 --- a/src/back/backsessionclasses.h +++ b/src/back/backsessionclasses.h @@ -49,10 +49,10 @@ class Session { private: bool getExePath(DWORD pid, /*out*/ std::wstring *exePath); - void fetchName(std::wstring exePath, DWORD pid); + bool fetchName(std::wstring exePath, DWORD pid); bool fetchNameViaFD(std::wstring exePath, DWORD pid, /*out*/ std::wstring *sessionName); bool fetchNameViaMSIX(std::wstring exePath, DWORD pid, /*out*/ std::wstring *sessionName); - bool fetchNameViaWindowName(std::wstring exePath, DWORD pid, /*out*/ std::wstring *sessionName); + bool fetchNameViaWindowName(DWORD pid, /*out*/ std::wstring *sessionName); /* std::wstring fetchProcessName(DWORD pid); */ std::wstring sessionName; SessionState sessionState; From d4db24ed7dc664a89f93674829774df0702ab748 Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 14 Aug 2024 17:02:57 +0200 Subject: [PATCH 12/38] wip: no endpoint, role rework, visual ratio --- assets.qrc | 1 + assets/selawk.ttf | Bin 0 -> 44224 bytes qtest.pro | 2 +- src/back/backlasses.cpp | 6 +- src/cont/contclasses.cpp | 18 +++- src/cont/contclasses.h | 10 ++- src/global.h | 2 + src/qt/qtclasses.cpp | 177 +++++++++++++++++++++++++++++---------- src/qt/qtclasses.h | 11 ++- src/qt/qtcommon.h | 1 + src/qt/qtvisuals.h | 2 + src/qtestmain.cpp | 11 ++- 12 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 assets/selawk.ttf diff --git a/assets.qrc b/assets.qrc index a9cbd38..4ef5956 100644 --- a/assets.qrc +++ b/assets.qrc @@ -1,5 +1,6 @@ + assets/selawk.ttf assets/notificationAreaIcon.png assets/style.qss assets/logo.ico diff --git a/assets/selawk.ttf b/assets/selawk.ttf new file mode 100644 index 0000000000000000000000000000000000000000..736bac3c210abb56f305c0c2307da8e002cd6c91 GIT binary patch literal 44224 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, wstringEndpointId); + osh->roleBucketEntryCallback(nRole, wstringEndpointId); + //osh->changeFrontDefaultsCallback(nRole, wstringEndpointId); return S_OK; } @@ -586,6 +588,8 @@ void Overseer::reloadEndpoints(Flows flow) { break; } deviceEnumerator->GetDefaultAudioEndpoint(MSflow, val, &temp); + if (!temp) continue; + LPWSTR id = nullptr; if (flow == Flows::FLOW_PLAYBACK) { diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index f46571c..fb0e22a 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -290,12 +290,22 @@ NGuid OverseerHandler::getGuid() { return this->os->getGuid(); } -void OverseerHandler::setChangeFrontDefaultsFunction(std::function changeFrontDefaults){ - this->changeFrontDefaults = changeFrontDefaults; +/* + * void OverseerHandler::setChangeFrontDefaultsFunction(std::function changeFrontDefaults){ + * this->changeFrontDefaults = changeFrontDefaults; + * } + * + * void OverseerHandler::changeFrontDefaultsCallback(Roles role, std::wstring endpointId) { + * this->changeFrontDefaults(role, endpointId); + * } + */ + +void OverseerHandler::roleBucketEntryCallback(Roles role, std::wstring endpointId){ + this->roleBucketEntry(role, endpointId); } -void OverseerHandler::changeFrontDefaultsCallback(Roles role, std::wstring endpointId) { - this->changeFrontDefaults(role, endpointId); +void OverseerHandler::setRoleBucketEntryFunction(std::function roleBucketEntry) { + this->roleBucketEntry = roleBucketEntry; } void OverseerHandler::updateFrontEndpointName(Endpoint* ep) { diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index b0cea92..e147394 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -99,9 +99,12 @@ public: OverseerHandler(); void openControlPanel(); - void setChangeFrontDefaultsFunction(std::function changeFrontDefaults); - void changeFrontDefaultsCallback(Roles role, std::wstring endpointId); + //void setChangeFrontDefaultsFunction(std::function changeFrontDefaults); + //void changeFrontDefaultsCallback(Roles role, std::wstring endpointId); + void roleBucketEntryCallback(Roles role, std::wstring endpointId); + void setRoleBucketEntryFunction(std::function roleBucketEntry); + void updateFrontEndpointName(Endpoint* ep); //void setReviseEndpointShowingFunction(std::function reviseEndpointShowing); void reviseEndpointShowing(std::wstring endpointId, EndpointState state); @@ -132,7 +135,8 @@ private: std::function changeFrontDefaults; std::function removeEndpointWidget; std::function addEndpointWidget; - + std::function roleBucketEntry; + /* Session's */ std::function changeSessionVolume; //std::function updateFrontVolumeCallback; diff --git a/src/global.h b/src/global.h index 5d327dc..4c22755 100644 --- a/src/global.h +++ b/src/global.h @@ -31,6 +31,8 @@ #define STRING_ABOUT "About" #define STRING_STARTUP "Run at startup" +#define STRING_NOENDPOINT "No active endpoints" + #define LSTRING_UNNAMED_SESSION L"Unnamed session" //INIT BACK diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index cbe8106..50a046a 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -245,10 +245,17 @@ void MainWindow::compose() { screenRes.getCoords(&srx1, &sry1, &srx2, &sry2); log_debugcpp("(for Percentage) Screen Res: " + std::to_string(srx1) + " " + std::to_string(srx2)+" " + std::to_string(sry1) + " " + std::to_string(sry2)); - uint64_t windowWidth = (uint64_t)std::abs(screenRes.width()) * this->widthRatio; + dpr = screen->devicePixelRatio(); + log_to_file("dpr: %f \n", dpr); + uint64_t windowWidth = ((uint64_t)std::abs(screenRes.width()) * widthRatio) * dpr; uint64_t screenHeight = (uint64_t)std::abs(screenRes.height()); log_debugcpp("Window Width: " + std::to_string(windowWidth)); + QFontMetrics fontMetrics = this->fontMetrics(); + int maxCharWidth = fontMetrics.maxWidth(); + int charHeight = fontMetrics.height(); + //QSize QFontMetrics::size(int flags, const QString &text, int tabStops = 0, int *tabArray = nullptr) const + this->createLayout(new QGridLayout()); //scrollArea->verticalScrollBar()->setMinimumWidth(windowWidth * (widthRatio)); //scrollArea->verticalScrollBar()->setMaximumWidth(windowWidth * (widthRatio)); @@ -259,8 +266,7 @@ void MainWindow::compose() { * this->hide(); * this->setAttribute(Qt::WA_DontShowOnScreen, false); */ - const qreal dpr = screen->devicePixelRatio(); - log_to_file("dpr: %f \n", dpr); + for (auto *epw : ews) { if (!epw) continue; epw->calculateSize(windowWidth, screenHeight); @@ -396,7 +402,7 @@ void SessionWidget::calculateSize(uint64_t width, uint64_t height) { /* og 1080p 120% testing values */ this->mainLabel->setMaximumWidth((int)(width * 0.30) /*1/16ish 1080p*/); this->mainLabel->setMinimumWidth((int)(width * 0.30) /*1/16ish 1080p*/); - this->mainLabel->setMaximumHeight((int)(height * 0.02)); + //this->mainLabel->setMaximumHeight((int)(height * 0.02)); this->mainLabel->setMinimumHeight((int)(height * 0.02)); this->muteButton->setMaximumWidth((int)(width * 0.10) /*1/32th 1080p*/); this->muteButton->setMinimumWidth((int)(width * 0.10) /*1/32th 1080p*/); @@ -410,7 +416,9 @@ void SessionWidget::calculateSize(uint64_t width, uint64_t height) { log_to_file("\tMute btn Maximum width: %d \n", muteButton->maximumWidth()); log_to_file("\tMute btn Minimum width: %d \n", muteButton->minimumWidth()); log_to_file("\tSlider Minimum width: %d \n", mainSlider->minimumWidth()); - log_to_file("\tSpacer Minimum width: %d \n\n", widthSpacer->minimumSize().width()); + log_to_file("\tSlider Maximum width: %d \n", mainSlider->maximumWidth()); + log_to_file("\tSpacer Minimum width: %d \n", widthSpacer->minimumSize().width()); + log_to_file("\tSpacer Maximum width: %d \n\n", widthSpacer->maximumSize().width()); } std::wstring SessionWidget::getName() { @@ -457,8 +465,10 @@ ChannelWidget::ChannelWidget(uint32_t channelCount, EndpointHandler* eph, QWidge tmp->setValue((int) volume); tmpLb->setText(QString::number(volume)); //tmpLb->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - tmp->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - tmpLb->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + //tmp->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + tmp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + //mainSlider->setFocusPolicy(Qt::StrongFocus); + tmpLb->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); this->channelSliders.push_back(tmp); this->channelLabels.push_back(tmpLb); widgetLayout->addWidget(tmp, row , col); @@ -505,7 +515,6 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i //todo: sussy this->eph->setState(EndpointState::ENDPOINT_ACTIVE, idx); this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - //setAttribute(Qt::WA_TranslucentBackground); widgetLayout = new QGridLayout(this); //this->setLayout(widgetLayout); log_debugcpp("epw main layout parent: " + std::to_string((intptr_t)(widgetLayout->parent()))); @@ -575,6 +584,7 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i uint32_t epChannelCount = eph->getChannelCount(); if(epChannelCount > 1) { cw = new ChannelWidget(epChannelCount, eph, nullptr); + cw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); //cw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); widgetLayout->addWidget(cw, row++, 0, 1, 4 /*colmax*/, Qt::AlignTop); } @@ -672,7 +682,8 @@ void EndpointWidget::addSessionWidget(CustomWidgetEvent* ev){ for (QWidget *widget : topLevelWidgets) { if (qobject_cast(widget)) { double widthRatio = ((MainWindow*)widget)->widthRatio; - sw->calculateSize(std::abs(this->screen()->geometry().width()) * widthRatio, + double dpr = ((MainWindow*)widget)->dpr; + sw->calculateSize((std::abs(this->screen()->geometry().width()) * widthRatio) * dpr, std::abs(this->screen()->geometry().height())); } } @@ -726,6 +737,9 @@ void MainWindow::customEvent(QEvent* ev) { } else if (ev->type() == (QEvent::Type)CustomQEvent::RecomposeMainWindow) { ev->setAccepted(true); if (this->isVisible()) this->compose(); + } else if (ev->type() == (QEvent::Type)CustomQEvent::EndpointRoleChange) { + ev->setAccepted(true); + this->flushRoleChanges(); } QMainWindow::customEvent(ev); } @@ -783,14 +797,27 @@ void MainWindow::reorderEndpointWidgetCollection() { void EndpointWidget::calculateSize(uint64_t width, uint64_t height) { /* og 1080p 120% testing values */ log_to_file("[EndpointWidget %s sizes]\n", converter.to_bytes(this->getEndpointHandler()->getName()).c_str()); - log_to_file("Params: {Width: %u Height: %u}\n", width, height); - this->mainLabel->setMaximumWidth((int)(width * 0.50) /* 1080p 120%*/); - this->mainLabel->setMinimumWidth((int)(width * 0.50) /* 1080p 120%*/); + log_to_file("Params: {WWidth: %u SHeight: %u}\n", width, height); + //this->setMaximumWidth(width); + /* + * this->mainLabel->setMaximumWidth((int)(width * 0.50) /\* 1080p 120%*\/); + * this->mainLabel->setMinimumWidth((int)(width * 0.50) /\* 1080p 120%*\/); + */ + this->mainLabel->setMaximumSize((int)(width * 0.50), height /* 1080p 120%*/); + this->mainLabel->setMinimumSize((int)(width * 0.50), 1 /* 1080p 120%*/); + this->muteButton->setMaximumSize((int)(width * 0.10), height /* 1080p 120%*/); + this->mainVolumeLabel->setMaximumSize((int)(width * 0.10), height /* 1080p 120%*/); + for (auto roleCheckbox : defaultRolesCheckBoxes) { + roleCheckbox.second->setMaximumWidth((int)(width * 0.20) /* 1080p 120%*/); + log_to_file("Role %d width: %d \n", roleCheckbox.first, roleCheckbox.second->maximumWidth()); + } log_to_file("Main label width: %d \n", this->mainLabel->maximumWidth()); + log_to_file("Mute button width: %d \n", this->muteButton->maximumWidth()); + log_to_file("Volume label width: %d \n", this->mainVolumeLabel->maximumWidth()); if (cw) { this->cw->setMinimumSize(QSize(1, (height * 0.06) * (int)((cw->getChannelCount() / 2) + 0.5))); - this->cw->setMaximumSize(QSize(QWIDGETSIZE_MAX, (height * 0.06) * (int)((cw->getChannelCount() / 2) + 0.5))); + //this->cw->setMaximumSize(QSize(width, (height * 0.06) * (int)((cw->getChannelCount() / 2) + 0.5))); log_to_file("Channels Maximum size: %d, %d \n", cw->maximumWidth(), cw->maximumHeight()); log_to_file("Channels Minimum size: %d, %d \n", cw->minimumWidth(), cw->minimumHeight()); } @@ -889,15 +916,23 @@ void MainWindow::createLayout(QGridLayout *newLayout) { widget->setLayout(newLayout); this->widgetLayout = newLayout; + bool areEndpoints = false; uint64_t i = 0; for (EndpointWidget *epw : ews) { if (!epw) continue; + else areEndpoints = true; log_debugcpp("EPWidget positioning"); log_wdebugcpp(L"epw name: " + epw->getEndpointHandler()->getName()); epw->setIndex(i); this->widgetLayout->addWidget(epw, i++, 0); } - widgetLayout->addItem(lastRowSpacer, i, 0); + if(areEndpoints) + widgetLayout->addItem(lastRowSpacer, i, 0); + else { + if (noEndpoints) delete noEndpoints; + noEndpoints = new QLabel(STRING_NOENDPOINT, this); + widgetLayout->addWidget(noEndpoints, i, 0); + } } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { @@ -910,6 +945,17 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { //setStyleSheet("background: transparent; "); //setStyleSheet("background-color: rgba(255,182,193);"); setWindowTitle(STRING_TITLE); + /* + * Font setup + */ + int id = QFontDatabase::addApplicationFont(":/assets/selawk.ttf"); + QString family = QFontDatabase::applicationFontFamilies(id).at(0); + font = QFont(family); + font.setKerning(true); + font.setPointSize(12); + this->setFont(font); + //qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "1"); + /* * Registering needed custom events */ @@ -920,9 +966,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QEvent::registerEventType(CustomQEvent::SessionWidgetObsolete); QEvent::registerEventType(CustomQEvent::SessionWidgetCreated); QEvent::registerEventType(CustomQEvent::RecomposeMainWindow); + QEvent::registerEventType(CustomQEvent::EndpointRoleChange); /* This spacer provides proper spacing when window vertically > widgets. */ lastRowSpacer = new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); + ewsUpdateTimer = new QTimer(this); recentlyClosedTimer = new QTimer(this); widget = new QWidget(); @@ -945,6 +993,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { * Scroll bar code */ scrollArea = new QScrollArea(this); + //widget->setAttribute(Qt::WA_TranslucentBackground); scrollArea->setWidget(widget); scrollArea->setWidgetResizable(true); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); @@ -959,6 +1008,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { * Menu bar code */ mainMenuBar = new QToolBar(this); + /* + * QPalette pal; + * pal.setColor(QPalette::Window, Qt::transparent); + * mainMenuBar->setPalette(pal); + */ hw = new HeaderWidget(this); mainMenuBar->addWidget(hw); mainMenuBar->setMovable(false); @@ -993,26 +1047,72 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { this->recentlyClosedTimer->start(); } }); + /* * Set of function callback definitons for EndpointSituationCallback */ - osh->setChangeFrontDefaultsFunction([this](Roles role, std::wstring endpointId) { - //Sigh... I hope to get this right when librole is done... - //todo: STILL BORKED. THEY'RE NOT FORFEITING THEIR OLD POSITION - EndpointWidget *newDef = nullptr, *oldDef = nullptr; - for (uint64_t i = 0; i < ews.size(); i++) { - auto epw = this->ews.at(i); - if (!epw) continue; - if (epw->getEndpointHandler()->getId() == endpointId) { - newDef = epw; - continue; } - if (epw->getEndpointHandler()->getRoles() & role) { - oldDef = epw; - continue; } - } + osh->setRoleBucketEntryFunction([this](Roles role, std::wstring endpointId) { + std::pair entry = { role, endpointId }; + this->roleBucketList.push_back(entry); + QCoreApplication::instance()->postEvent(this, new QEvent((QEvent::Type)CustomQEvent::EndpointRoleChange),Qt::LowEventPriority); + }); - //debug if (role != Roles::ROLE_COMMUNICATIONS) return; - if (oldDef && newDef) { + osh->setRemoveEndpointWidgetFunction([this](uint64_t index) { + QCoreApplication::instance()->postEvent(this, new CustomWidgetEvent((QEvent::Type)CustomQEvent::EndpointWidgetObsolete, index)); + }); + + osh->setAddEndpointWidgetFunction([this](EndpointHandler* eph) { + QCoreApplication::instance()->postEvent(this, new CustomWidgetEvent((QEvent::Type)CustomQEvent::EndpointWidgetCreated, eph)); + }); + +} + +void MainWindow::flushRoleChanges() { + std::pair change = roleBucketList.back(); + roleBucketList.pop_back(); + this->changeFrontDefaults(change.first, change.second); +} + +void MainWindow::changeFrontDefaults(Roles role, std::wstring endpointId) { + //Sigh... I hope to get this right when librole is done... + //todo: STILL BORKED. THEY'RE NOT FORFEITING THEIR OLD POSITION + EndpointWidget *newDef = nullptr, *oldDef = nullptr; + for (uint64_t i = 0; i < ews.size(); i++) { + auto epw = this->ews.at(i); + if (!epw) continue; + if (epw->getEndpointHandler()->getId() == endpointId) { + newDef = epw; + continue; } + if (epw->getEndpointHandler()->getRoles() & role) { + oldDef = epw; + continue; } + } + + + //debug if (role != Roles::ROLE_COMMUNICATIONS) return; + if (newDef && !oldDef) { + newDef->getDefaultRolesWidgets().at(role)->blockSignals(true); + newDef->getEndpointHandler()->assignRoles(role); + QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + uint8_t newDefIdx = newDef->getIndex(); + uint8_t newDefRoles = newDef->getEndpointHandler()->getRoles(); + if (newDefRoles == Roles::ROLE_ALL) { + newDef->setIndex(0); + this->ews[0] = newDef; + if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; + QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + } else if ((newDefRoles & Roles::ROLE_MULTIMEDIA) || + (newDefRoles & Roles::ROLE_CONSOLE)){ + newDef->setIndex(0); + this->ews[0] = newDef; + } else if (newDefRoles & Roles::ROLE_COMMUNICATIONS) { + newDef->setIndex(1); + this->ews[1] = newDef; + } + log_debugcpp("newDef new idx: " + std::to_string(newDef->getIndex())); + newDef->getDefaultRolesWidgets().at(role)->blockSignals(false); + } + else if (oldDef && newDef) { this->ews.at(oldDef->getIndex()) = nullptr; this->ews.at(newDef->getIndex()) = nullptr; newDef->getDefaultRolesWidgets().at(role)->blockSignals(true); @@ -1023,6 +1123,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { if (newDefRoles == Roles::ROLE_ALL) { newDef->setIndex(0); this->ews[0] = newDef; + if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); } else if ((newDefRoles & Roles::ROLE_MULTIMEDIA) || (newDefRoles & Roles::ROLE_CONSOLE)){ @@ -1060,19 +1161,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { } log_debugcpp("oldDef new idx: " + std::to_string(oldDef->getIndex())); oldDef->getDefaultRolesWidgets().at(role)->blockSignals(false); - } - QCoreApplication::instance()->postEvent - (this, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); - }); - - osh->setRemoveEndpointWidgetFunction([this](uint64_t index) { - QCoreApplication::instance()->postEvent(this, new CustomWidgetEvent((QEvent::Type)CustomQEvent::EndpointWidgetObsolete, index)); - }); - - osh->setAddEndpointWidgetFunction([this](EndpointHandler* eph) { - QCoreApplication::instance()->postEvent(this, new CustomWidgetEvent((QEvent::Type)CustomQEvent::EndpointWidgetCreated, eph)); - }); - + } + QCoreApplication::instance()->postEvent + (this, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); } void MainWindow::closeEvent(QCloseEvent *event) { diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 6a5e9aa..c1b6c6e 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -18,7 +18,8 @@ enum CustomQEvent { EndpointDefaultChange = 1003, SessionWidgetCreated = 1004, SessionWidgetObsolete = 1005, - RecomposeMainWindow = 1006 + RecomposeMainWindow = 1006, + EndpointRoleChange = 1007 }; template @@ -198,7 +199,11 @@ private slots: private: //std::vector *ephs; + void flushRoleChanges(); + void changeFrontDefaults(Roles role, std::wstring endpointId); + std::vector ews; + std::vector> roleBucketList; QWidget *widget; QGridLayout *widgetLayout; @@ -209,6 +214,7 @@ private: QTimer *ewsUpdateTimer; static constexpr uint64_t ewsUpdateTimerFrequency = 500; double widthRatio = 0.28; + double dpr = 1.0; bool recentlyClosed = false; uint8_t recentlyClosedTimerFrequency = 1000; QTimer *recentlyClosedTimer; @@ -218,6 +224,9 @@ private: QToolBar *mainMenuBar; QScreen *screen; QSpacerItem* lastRowSpacer; + QLabel* noEndpoints = nullptr; + + QFont font; friend class EndpointWidget; //public slots: // void setEndpointHandlers(std::vector *ephs); diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h index 414e236..b7a7245 100644 --- a/src/qt/qtcommon.h +++ b/src/qt/qtcommon.h @@ -41,6 +41,7 @@ #include #include #include +#include //#include //#include /* diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h index 4c7ac4f..3095c45 100644 --- a/src/qt/qtvisuals.h +++ b/src/qt/qtvisuals.h @@ -181,6 +181,8 @@ public: break; } + return QRect(); + } void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index 6c435c3..490608d 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -46,16 +46,19 @@ int main (int argc, char* argv[]) { * log_debugcpp(a.toStdString()); * } */ - + QApplication::setStyle(new MixerStyle(QStyleFactory::create("Fusion"))); + //QApplication::setFont(font); QPalette palette = QGuiApplication::palette(); //todo: ez full apply os accent colorw palette.setColor(QPalette::Active, QPalette::Highlight, QColor(255, 192, 203, 200));//QColor(30,30,30,100)); QGuiApplication::setPalette(palette); - //Check if running - //https://stackoverflow.com/questions/48060989/qt-show-application-if-currently-running + initialize_file_log(); atexit(closeDebugFileLog); + + //Check if running + //https://stackoverflow.com/questions/48060989/qt-show-application-if-currently-running //std::set_terminate(closeDebugFileLog2); if (!isSingleInstanceRunning("Mixer")) startSingleInstanceServer("Mixer"); @@ -71,7 +74,7 @@ int main (int argc, char* argv[]) { //INIT FRONT QScopedPointer app(createApplication(argc, argv)); - + MainWindow window = MainWindow(); //window.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::QSizePolicy::MinimumExpanding) QApplication::setQuitOnLastWindowClosed(false); From 0bf8b321ae0c2d920919f7af1331270a815efaa2 Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 15 Aug 2024 18:07:13 +0200 Subject: [PATCH 13/38] wip: endpoint add/remove/role change bugfixes --- src/qt/qtclasses.cpp | 54 ++++++++++++++++++++++++-------------------- src/qt/qtclasses.h | 2 +- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 50a046a..8692897 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -753,14 +753,15 @@ void MainWindow::removeEndpointWidget(CustomWidgetEvent* ev){ //delete ews.at(index); delete ews.at(i); ews.at(i) = nullptr; - this->ewsUpdateTimer->start(); + //TODO: is a flattener really necessary? + //this->ewsUpdateTimer->start(); return; } void MainWindow::addEndpointWidget(CustomWidgetEvent* ev){ EndpointWidget* epw = new EndpointWidget(ev->payload, widget, this->ews.size()); //epw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - this->widgetLayout->addWidget(epw); + //this->widgetLayout->addWidget(epw); ews.push_back(epw); return; } @@ -911,6 +912,7 @@ HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { } void MainWindow::createLayout(QGridLayout *newLayout) { + log_debugcpp("createLayout"); widgetLayout->removeItem(lastRowSpacer); delete this->widgetLayout; widget->setLayout(newLayout); @@ -921,16 +923,15 @@ void MainWindow::createLayout(QGridLayout *newLayout) { for (EndpointWidget *epw : ews) { if (!epw) continue; else areEndpoints = true; - log_debugcpp("EPWidget positioning"); log_wdebugcpp(L"epw name: " + epw->getEndpointHandler()->getName()); - epw->setIndex(i); + //epw->setIndex(i); this->widgetLayout->addWidget(epw, i++, 0); } - if(areEndpoints) + if(areEndpoints) { + if (noEndpoints) { delete noEndpoints; noEndpoints = nullptr; } widgetLayout->addItem(lastRowSpacer, i, 0); - else { - if (noEndpoints) delete noEndpoints; - noEndpoints = new QLabel(STRING_NOENDPOINT, this); + } else { + if (!noEndpoints) noEndpoints = new QLabel(STRING_NOENDPOINT, this); widgetLayout->addWidget(noEndpoints, i, 0); } } @@ -1068,27 +1069,32 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { } void MainWindow::flushRoleChanges() { - std::pair change = roleBucketList.back(); - roleBucketList.pop_back(); - this->changeFrontDefaults(change.first, change.second); -} - -void MainWindow::changeFrontDefaults(Roles role, std::wstring endpointId) { - //Sigh... I hope to get this right when librole is done... - //todo: STILL BORKED. THEY'RE NOT FORFEITING THEIR OLD POSITION + //TODO: bucket list deque + std::pair change = roleBucketList.front(); + roleBucketList.erase(roleBucketList.begin()); + EndpointWidget *newDef = nullptr, *oldDef = nullptr; for (uint64_t i = 0; i < ews.size(); i++) { auto epw = this->ews.at(i); if (!epw) continue; - if (epw->getEndpointHandler()->getId() == endpointId) { + if (epw->getEndpointHandler()->getId() == change.second) { newDef = epw; - continue; } - if (epw->getEndpointHandler()->getRoles() & role) { + ews.at(i) = nullptr; + continue; + } + if (epw->getEndpointHandler()->getRoles() & change.first) { oldDef = epw; - continue; } + ews.at(i) = nullptr; + continue; + } } - + this->changeFrontDefaults(change.first, newDef, oldDef); +} + +void MainWindow::changeFrontDefaults(Roles role, EndpointWidget* newDef, EndpointWidget* oldDef) { + //Sigh... I hope to get this right when librole is done... + //todo: STILL BORKED. THEY'RE NOT FORFEITING THEIR OLD POSITION //debug if (role != Roles::ROLE_COMMUNICATIONS) return; if (newDef && !oldDef) { newDef->getDefaultRolesWidgets().at(role)->blockSignals(true); @@ -1099,7 +1105,7 @@ void MainWindow::changeFrontDefaults(Roles role, std::wstring endpointId) { if (newDefRoles == Roles::ROLE_ALL) { newDef->setIndex(0); this->ews[0] = newDef; - if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; + //if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); } else if ((newDefRoles & Roles::ROLE_MULTIMEDIA) || (newDefRoles & Roles::ROLE_CONSOLE)){ @@ -1113,8 +1119,6 @@ void MainWindow::changeFrontDefaults(Roles role, std::wstring endpointId) { newDef->getDefaultRolesWidgets().at(role)->blockSignals(false); } else if (oldDef && newDef) { - this->ews.at(oldDef->getIndex()) = nullptr; - this->ews.at(newDef->getIndex()) = nullptr; newDef->getDefaultRolesWidgets().at(role)->blockSignals(true); newDef->getEndpointHandler()->assignRoles(role); QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); @@ -1123,7 +1127,7 @@ void MainWindow::changeFrontDefaults(Roles role, std::wstring endpointId) { if (newDefRoles == Roles::ROLE_ALL) { newDef->setIndex(0); this->ews[0] = newDef; - if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; + //if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); } else if ((newDefRoles & Roles::ROLE_MULTIMEDIA) || (newDefRoles & Roles::ROLE_CONSOLE)){ diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index c1b6c6e..8b77923 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -200,7 +200,7 @@ private slots: private: //std::vector *ephs; void flushRoleChanges(); - void changeFrontDefaults(Roles role, std::wstring endpointId); + void changeFrontDefaults(Roles role, EndpointWidget* newDef, EndpointWidget* oldDef); std::vector ews; std::vector> roleBucketList; From 13855c2e6fc63d1ea5f928ec35fc8615d1b82c9d Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 20 Nov 2024 19:21:37 +0100 Subject: [PATCH 14/38] wip: dark/light mode adaptation --- src/back/backlasses.cpp | 38 ++++++++++++++++++++++++++++++++++++++ src/back/backlasses.h | 9 ++++++++- src/back/msinclude.h | 1 + src/cont/contclasses.cpp | 12 ++++++++++++ src/cont/contclasses.h | 3 +++ src/global.h | 5 +++++ src/qt/qtclasses.cpp | 22 ++++++++++++++++++++++ src/qt/qtclasses.h | 11 ++++++----- src/qt/qtcommon.h | 16 ++++++++++++++++ src/qtestmain.cpp | 22 ++++++++++++++-------- 10 files changed, 125 insertions(+), 14 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 1654b42..970d79f 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -652,6 +652,10 @@ Overseer::Overseer() : epsc(this){ if(FAILED(deviceEnumerator->RegisterEndpointNotificationCallback(((IMMNotificationClient*)&epsc)))) { log_debugcpp("when no enchufas......"); } } +void Overseer::populateSystemValues() { + updateDarkMode(); +} + void Overseer::openControlPanel() { STARTUPINFOEXW startupConfig; PROCESS_INFORMATION processInfo; @@ -678,6 +682,40 @@ void Overseer::openControlPanel() { } } +ProcessedNativeEvent Overseer::processTopLevelWindowMessage(void* msg) { +#ifdef WIN32 + MSG *message = static_cast(msg); + switch(message->message) { + case WM_SETTINGCHANGE: + if(!wcscmp(((wchar_t*)message->lParam), L"ImmersiveColorSet")) + return updateDarkMode(); + break; + default: + return ProcessedNativeEvent::NONE; + break; + } + return ProcessedNativeEvent::NONE; + //if (message->message != WM_SETTINGCHANGE) {return false;} +#endif +} + +ProcessedNativeEvent Overseer::updateDarkMode(){ + // DwmGetColorizationColor( WM_DWMCOLORIZATIONCOLORCHANGED + DWORD value = 0; + DWORD size = sizeof(DWORD); + + LSTATUS result; + + result = RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size); + + this->lightMode = (bool)value; + return ProcessedNativeEvent::LIGHT_MODE; +} + +bool Overseer::isLightMode() { + return this->lightMode; +} + NGuid Overseer::getGuid() { return guid; } diff --git a/src/back/backlasses.h b/src/back/backlasses.h index bcc3306..9edc779 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -109,14 +109,20 @@ class Overseer { public: Overseer(); + NGuid getGuid(); + void populateSystemValues(); void openControlPanel(); + ProcessedNativeEvent processTopLevelWindowMessage(void* msg); + ProcessedNativeEvent updateDarkMode(); + bool isLightMode(); + std::vector getPlaybackEndpoints(); std::vector getCaptureEndpoints(); void updateEndpointInfo(std::wstring endpointId); void reloadEndpoints(Flows flow); Endpoint* addEndpoint(std::wstring endpointId, /* out */ Flows* flow); - NGuid getGuid(); + //void setEndpointStatusCallback(); //void setEndpointStatusCallback(); @@ -131,6 +137,7 @@ class Overseer { void initCOMLibrary(); NGuid guid; + bool lightMode; IMMDeviceEnumerator *deviceEnumerator; EndpointSituationCallback epsc; diff --git a/src/back/msinclude.h b/src/back/msinclude.h index 6f21ae1..f9ab1b4 100644 --- a/src/back/msinclude.h +++ b/src/back/msinclude.h @@ -1,6 +1,7 @@ #pragma once #define _WIN32_WINNT 0x0A00 + #include //done by qt by def #define UNICODE diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index fb0e22a..75df484 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -205,10 +205,22 @@ OverseerHandler::OverseerHandler() { this->os = new Overseer(); } +void OverseerHandler::populateSystemValues() { + this->os->populateSystemValues(); +} + void OverseerHandler::openControlPanel() { this->os->openControlPanel(); } +ProcessedNativeEvent OverseerHandler::processTopLevelWindowMessage(void* msg) { + return this->os->processTopLevelWindowMessage(msg); +} + +bool OverseerHandler::isLightMode() { + return this->os->isLightMode(); +} + std::vector OverseerHandler::getPlaybackEndpoints() { return this->os->getPlaybackEndpoints(); } diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index e147394..cb3230a 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -97,7 +97,10 @@ class OverseerHandler { public: OverseerHandler(); + void populateSystemValues(); void openControlPanel(); + ProcessedNativeEvent processTopLevelWindowMessage(void* msg); + bool isLightMode(); //void setChangeFrontDefaultsFunction(std::function changeFrontDefaults); //void changeFrontDefaultsCallback(Roles role, std::wstring endpointId); diff --git a/src/global.h b/src/global.h index 4c22755..9cecbfd 100644 --- a/src/global.h +++ b/src/global.h @@ -36,6 +36,11 @@ #define LSTRING_UNNAMED_SESSION L"Unnamed session" //INIT BACK +enum ProcessedNativeEvent { + NONE = 0, + LIGHT_MODE = (1 << 0), +}; + enum AudioChannel { CHANNEL_LEFT = (1 << 0), CHANNEL_RIGHT = (1 << 1), diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 8692897..835daff 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -1,7 +1,29 @@ #include "qtclasses.h" #include "meterslider.h" + #define POLLING_RATE 2 +bool DarkModeEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *) { + if (eventType == "windows_generic_MSG") { + ProcessedNativeEvent event = osh->processTopLevelWindowMessage(message); + switch(event) { + case LIGHT_MODE: + StylingHelper::setBackgroundColor(osh->isLightMode()); + return true; + break; + default: + break; + } + /* + * if ([event type] == NSKeyDown) { + * // Handle key event + * qDebug() << QString::fromNSString([event characters]); + * } + */ + } + return false; +} + template CustomWidgetEvent::CustomWidgetEvent(QEvent::Type type, T payload) : QEvent(type){ this->payload = payload; diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 8b77923..3b60cff 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -22,6 +22,12 @@ enum CustomQEvent { EndpointRoleChange = 1007 }; +class DarkModeEventFilter : public QAbstractNativeEventFilter { + +public: + bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *) override; +}; + template class CustomWidgetEvent : public QEvent { @@ -31,10 +37,6 @@ public: }; //Q_DECLARE_METATYPE(EndpointWidgetEvent) - -//todo: TEST. TEST. -//#include "qtvisuals.h" - class ExtendedCheckBox : public QCheckBox { Q_OBJECT protected: @@ -47,7 +49,6 @@ public: //B(int x) : A(x) { } }; - class SessionWidget : public QWidget { Q_OBJECT diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h index b7a7245..afd7929 100644 --- a/src/qt/qtcommon.h +++ b/src/qt/qtcommon.h @@ -42,6 +42,8 @@ #include #include #include +#include +#include //#include //#include /* @@ -62,6 +64,20 @@ enum CustomComplexControl { }; namespace StylingHelper { + + static inline void setBackgroundColor(bool lightMode) { + //QApplication* app = (QApplication*)QApplication::instance(); + QPalette pal = QGuiApplication::palette(); + if(lightMode) { + pal.setColor(QPalette::Window, Qt::white); + pal.setColor(QPalette::WindowText, Qt::black); + } else { + pal.setColor(QPalette::Window, Qt::black); + pal.setColor(QPalette::WindowText, Qt::white); + } + QGuiApplication::setPalette(pal); + } + static inline QLatin1String operator""_L1(const char* ch, uint64_t) { return QLatin1String(ch); } diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index 490608d..387df74 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -46,14 +46,6 @@ int main (int argc, char* argv[]) { * log_debugcpp(a.toStdString()); * } */ - - QApplication::setStyle(new MixerStyle(QStyleFactory::create("Fusion"))); - //QApplication::setFont(font); - QPalette palette = QGuiApplication::palette(); - //todo: ez full apply os accent colorw - palette.setColor(QPalette::Active, QPalette::Highlight, QColor(255, 192, 203, 200));//QColor(30,30,30,100)); - QGuiApplication::setPalette(palette); - initialize_file_log(); atexit(closeDebugFileLog); @@ -64,7 +56,17 @@ int main (int argc, char* argv[]) { startSingleInstanceServer("Mixer"); else exit(0); + + QApplication::setStyle(new MixerStyle(QStyleFactory::create("Fusion"))); + //QApplication::setFont(font); + //QPalette palette = QGuiApplication::palette(); + //todo: ez full apply os accent colorw + //palette.setColor(QPalette::Active, QPalette::Highlight, QColor(255, 192, 203, 200)); + //QColor(30,30,30,100)); + //QGuiApplication::setPalette(palette); osh = new OverseerHandler(); + osh->populateSystemValues(); + StylingHelper::setBackgroundColor(osh->isLightMode()); //qRegisterMetaType(); //INIT CONT @@ -78,6 +80,10 @@ int main (int argc, char* argv[]) { MainWindow window = MainWindow(); //window.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::QSizePolicy::MinimumExpanding) QApplication::setQuitOnLastWindowClosed(false); + + DarkModeEventFilter* darkMode = new DarkModeEventFilter(); + QAbstractEventDispatcher::instance()->installNativeEventFilter(darkMode); + /* * QFile styleFile(":/assets/style.qss"); * styleFile.open(QFile::ReadOnly); From 60890cecad9ff2172b8be3916a7da92ae6d6c781 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 26 Nov 2024 17:11:01 +0100 Subject: [PATCH 15/38] dark/light mode & accent color --- src/back/backlasses.cpp | 21 ++++++++-- src/back/backlasses.h | 4 +- src/cont/contclasses.cpp | 4 ++ src/cont/contclasses.h | 1 + src/global.h | 4 +- src/qt/qtclasses.cpp | 88 ++++++++++++++++++++++++++++++++++++---- src/qt/qtclasses.h | 2 + src/qt/qtcommon.h | 23 +++++++++++ src/qt/qtvisuals.h | 75 +++++++++++++++++++++------------- 9 files changed, 178 insertions(+), 44 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 970d79f..e51c011 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -653,7 +653,7 @@ Overseer::Overseer() : epsc(this){ } void Overseer::populateSystemValues() { - updateDarkMode(); + updateColors(); } void Overseer::openControlPanel() { @@ -688,7 +688,7 @@ ProcessedNativeEvent Overseer::processTopLevelWindowMessage(void* msg) { switch(message->message) { case WM_SETTINGCHANGE: if(!wcscmp(((wchar_t*)message->lParam), L"ImmersiveColorSet")) - return updateDarkMode(); + return updateColors(); break; default: return ProcessedNativeEvent::NONE; @@ -699,23 +699,36 @@ ProcessedNativeEvent Overseer::processTopLevelWindowMessage(void* msg) { #endif } -ProcessedNativeEvent Overseer::updateDarkMode(){ +ProcessedNativeEvent Overseer::updateColors() { // DwmGetColorizationColor( WM_DWMCOLORIZATIONCOLORCHANGED DWORD value = 0; DWORD size = sizeof(DWORD); LSTATUS result; + //Theme bg color result = RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size); this->lightMode = (bool)value; - return ProcessedNativeEvent::LIGHT_MODE; + + //Accent color + result = RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\DWM", L"ColorizationColor", RRF_RT_REG_DWORD, nullptr, &value, &size); + + if (result == ERROR_SUCCESS) { + this->accentColor = value; + } else this->accentColor = 0xffffffff; + + return ProcessedNativeEvent::COLORS; } bool Overseer::isLightMode() { return this->lightMode; } +uint32_t Overseer::getAccentColor() { + return this->accentColor; +} + NGuid Overseer::getGuid() { return guid; } diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 9edc779..f5fba61 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -113,8 +113,9 @@ class Overseer { void populateSystemValues(); void openControlPanel(); ProcessedNativeEvent processTopLevelWindowMessage(void* msg); - ProcessedNativeEvent updateDarkMode(); + ProcessedNativeEvent updateColors(); bool isLightMode(); + uint32_t getAccentColor(); std::vector getPlaybackEndpoints(); std::vector getCaptureEndpoints(); @@ -138,6 +139,7 @@ class Overseer { NGuid guid; bool lightMode; + uint32_t accentColor; IMMDeviceEnumerator *deviceEnumerator; EndpointSituationCallback epsc; diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 75df484..71f981d 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -221,6 +221,10 @@ bool OverseerHandler::isLightMode() { return this->os->isLightMode(); } +uint32_t OverseerHandler::getAccentColor() { + return this->os->getAccentColor(); +} + std::vector OverseerHandler::getPlaybackEndpoints() { return this->os->getPlaybackEndpoints(); } diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index cb3230a..e3ebab6 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -101,6 +101,7 @@ public: void openControlPanel(); ProcessedNativeEvent processTopLevelWindowMessage(void* msg); bool isLightMode(); + uint32_t getAccentColor(); //void setChangeFrontDefaultsFunction(std::function changeFrontDefaults); //void changeFrontDefaultsCallback(Roles role, std::wstring endpointId); diff --git a/src/global.h b/src/global.h index 9cecbfd..4d684a9 100644 --- a/src/global.h +++ b/src/global.h @@ -37,8 +37,8 @@ //INIT BACK enum ProcessedNativeEvent { - NONE = 0, - LIGHT_MODE = (1 << 0), + NONE = 0, + COLORS = (1 << 0), }; enum AudioChannel { diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 835daff..48660f1 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -7,19 +7,27 @@ bool DarkModeEventFilter::nativeEventFilter(const QByteArray &eventType, void *m if (eventType == "windows_generic_MSG") { ProcessedNativeEvent event = osh->processTopLevelWindowMessage(message); switch(event) { - case LIGHT_MODE: + case COLORS: StylingHelper::setBackgroundColor(osh->isLightMode()); + + //Update accent color across entire app + { + uint32_t color = osh->getAccentColor(); + uint8_t r, g, b, a; + if (StylingHelper::argbToDiscreteValues(osh->getAccentColor(), &r, &g, &b, &a)) { + const QWidgetList topLevelWidgets = QApplication::topLevelWidgets(); + for (QWidget *widget : topLevelWidgets) { + if(qobject_cast(widget)) { + ((MainWindow*)widget)->updateColor(r, g, b, a); + } + } + } + } return true; break; default: break; } - /* - * if ([event type] == NSKeyDown) { - * // Handle key event - * qDebug() << QString::fromNSString([event characters]); - * } - */ } return false; } @@ -124,8 +132,8 @@ void MeterSlider::paintEvent(QPaintEvent *event) { sliderComplex2.subControls |= QStyle::SC_SliderTickmarks; QPainter painter(this); - QStyle* stle = QApplication::style(); - stle->drawComplexControl((QStyle::ComplexControl)CC_MeterSlider, &sliderComplex2, &painter, this); + QStyle* style = QApplication::style(); + style->drawComplexControl((QStyle::ComplexControl)CC_MeterSlider, &sliderComplex2, &painter, this); //Q_D(QSlider); @@ -252,6 +260,13 @@ QRect MainWindow::setSizePosition(QScreen* screen, int width, int height) { } } +void MainWindow::updateColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + QColor color(r, g, b, a); + QPalette pal = QPalette(color); + pal.setColor(QPalette::Highlight, color); + scrollArea->verticalScrollBar()->setPalette(pal); +} + void MainWindow::compose() { //todo: invalidate layout when adding sessions with window open //We need dynamically added child widgets to expand so that we know their height @@ -1022,6 +1037,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setContentsMargins(QMargins(0, 0, 0, 0)); + + //ScrollBarFocusFilter focusFilter = new ScrollBarFocusFilter(this); + scrollArea->verticalScrollBar()->installEventFilter(this); + scrollArea->horizontalScrollBar()->installEventFilter(this); //scrollArea->verticalScrollBar()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); //scrollArea->verticalScrollBar()->setSingleStep(1); @@ -1070,6 +1089,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { this->recentlyClosedTimer->start(); } }); + + /* + * Set accent color + */ + uint8_t a, r, g, b; + if (StylingHelper::argbToDiscreteValues(osh->getAccentColor(), &r, &g, &b, &a)) + this->updateColor(r, g, b, a); /* * Set of function callback definitons for EndpointSituationCallback @@ -1090,6 +1116,50 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { } +bool MainWindow::eventFilter(QObject *object, QEvent *event) { + //QScrollBar* widgetCast = qobject_cast(object); + if (object == scrollArea->verticalScrollBar()) { + [[maybe_unused]] QEvent::Type tipo = event->type(); + if (event->type() == QHoverEvent::HoverEnter) { + log_debugcpp("Hover event: "); + log_debugcpp("\tGlobal pos: " + + std::to_string(((QHoverEvent*)event)->globalPosition().x()) + + ", " + + std::to_string(((QHoverEvent*)event)->globalPosition().y())); + log_debugcpp("\tNewWid pos: " + + std::to_string(((QHoverEvent*)event)->position().x()) + + ", " + + std::to_string(((QHoverEvent*)event)->position().y())); + log_debugcpp("\tOldWid pos: " + + std::to_string(((QHoverEvent*)event)->oldPos().x()) + + ", " + + std::to_string(((QHoverEvent*)event)->oldPos().y())); + + } + if (event->type() == QScrollEvent::ScrollFinished) { + QHoverEvent* hoverEvent = new QHoverEvent(QEvent::HoverEnter, + scrollArea->verticalScrollBar()->mapFromGlobal(QCursor::pos()), + QCursor::pos(), + scrollArea->verticalScrollBar()->mapFromGlobal(QCursor::pos())); + log_debugcpp("ScrollFinished event: "); + log_debugcpp("\tGlobal pos: " + + std::to_string(QCursor::pos().x()) + + ", " + + std::to_string(QCursor::pos().y())); + log_debugcpp("\tWidget pos: " + + std::to_string(scrollArea->verticalScrollBar()->mapFromGlobal(QCursor::pos()).x()) + + ", " + + std::to_string(scrollArea->verticalScrollBar()->mapFromGlobal(QCursor::pos()).y())); + QCoreApplication::instance()->postEvent(scrollArea->verticalScrollBar(), hoverEvent); + /* + * QCoreApplication::instance()->postEvent(this, new CustomWidgetEvent((QEvent::Type)CustomQEvent::SessionWidgetObsolete, sessionHandler)); + */ + //scrollArea->setFocus(Qt::MouseFocusReason); + return false; + }} + return false; +} + void MainWindow::flushRoleChanges() { //TODO: bucket list deque std::pair change = roleBucketList.front(); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 3b60cff..349b2d3 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -182,6 +182,7 @@ public: MainWindow(QWidget *parent = nullptr); void reloadEndpointWidgets(); void compose(); + void updateColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a); protected: void closeEvent(QCloseEvent *event) override; @@ -200,6 +201,7 @@ private slots: private: //std::vector *ephs; + bool eventFilter(QObject *object, QEvent *event); void flushRoleChanges(); void changeFrontDefaults(Roles role, EndpointWidget* newDef, EndpointWidget* oldDef); diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h index afd7929..b059504 100644 --- a/src/qt/qtcommon.h +++ b/src/qt/qtcommon.h @@ -77,6 +77,29 @@ namespace StylingHelper { } QGuiApplication::setPalette(pal); } + + static inline void setAccentColor(bool lightMode) { + //QApplication* app = (QApplication*)QApplication::instance(); + QPalette pal = QGuiApplication::palette(); + if(lightMode) { + pal.setColor(QPalette::Window, Qt::white); + pal.setColor(QPalette::WindowText, Qt::black); + } else { + pal.setColor(QPalette::Window, Qt::black); + pal.setColor(QPalette::WindowText, Qt::white); + } + QGuiApplication::setPalette(pal); + } + + static inline bool argbToDiscreteValues(uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a) { + if(!r || !g || !b || !a) return false; + + *a = (color >> 24) & 0xFF; + *r = (color >> 16) & 0xFF; + *g = (color >> 8) & 0xFF; + *b = color & 0xFF; + return true; + } static inline QLatin1String operator""_L1(const char* ch, uint64_t) { return QLatin1String(ch); diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h index 3095c45..0db58e8 100644 --- a/src/qt/qtvisuals.h +++ b/src/qt/qtvisuals.h @@ -6,6 +6,7 @@ using namespace StylingHelper; class MixerStyle : public QProxyStyle { + public: using QProxyStyle::QProxyStyle; @@ -448,8 +449,8 @@ public: //QColor arrowColor = QColor(188,143,143,100); arrowColor.setAlpha(160); - const QColor bgColor = backgroundColor(option->palette, widget); - const bool isDarkBg = bgColor.red() < 128 && bgColor.green() < 128 && bgColor.blue() < 128; + const QColor appBgColor = QGuiApplication::palette().window().color(); + const bool isDarkBg = appBgColor.red() < 128 && appBgColor.green() < 128 && appBgColor.blue() < 128; if (transient) { if (horizontal) { @@ -482,17 +483,17 @@ public: gradient.setColorAt(0.9, buttonColor.darker(105)); gradient.setColorAt(1, buttonColor.darker(107)); } else { - gradient.setColorAt(0, bgColor.lighter(157)); - gradient.setColorAt(0.1, bgColor.lighter(155)); - gradient.setColorAt(0.9, bgColor.lighter(155)); - gradient.setColorAt(1, bgColor.lighter(157)); + gradient.setColorAt(0, appBgColor.lighter(157)); + gradient.setColorAt(0.1, appBgColor.lighter(155)); + gradient.setColorAt(0.9, appBgColor.lighter(155)); + gradient.setColorAt(1, appBgColor.lighter(157)); } - + painter->save(); //painter->fillRect(rect, gradient); painter->setPen(Qt::NoPen); painter->setPen(alphaOutline); - + QColor subtleEdge = alphaOutline; subtleEdge.setAlpha(40); painter->setPen(subtleEdge); @@ -505,23 +506,27 @@ public: } QRect pixmapRect = scrollBarSlider; - QLinearGradient gradient(pixmapRect.center().x(), pixmapRect.top(), - pixmapRect.center().x(), pixmapRect.bottom()); - if (!horizontal) - gradient = QLinearGradient(pixmapRect.left(), pixmapRect.center().y(), - pixmapRect.right(), pixmapRect.center().y()); + QColor highlightColor = option->palette.highlight().color(); + /* + * QLinearGradient gradient(pixmapRect.center().x(), pixmapRect.top(), + * pixmapRect.center().x(), pixmapRect.bottom()); + * if (!horizontal) + * gradient = QLinearGradient(pixmapRect.left(), pixmapRect.center().y(), + * pixmapRect.right(), pixmapRect.center().y()); + */ - QLinearGradient highlightedGradient = gradient; + //QLinearGradient highlightedGradient = gradient; - QColor midColor2 = mergedColors(gradientStartColor, gradientStopColor, 40); - gradient.setColorAt(0, calculateButtonColor(option->palette).lighter(108)); - gradient.setColorAt(1, calculateButtonColor(option->palette)); - - highlightedGradient.setColorAt(0, gradientStartColor.darker(102)); - highlightedGradient.setColorAt(1, gradientStopColor.lighter(102)); + QColor heldColor = 0xc4e3d7e0;//Qt::gray;//osh->isLightMode() ? Qt::white : Qt::black; + //gradient.setColorAt(0, option->palette.highlight().color()); + //gradient.setColorAt(1, option->palette.highlight().color()); + + //highlightedGradient.setColorAt(0, gradientStartColor.darker(102)); + //highlightedGradient.setColorAt(1, gradientStopColor.lighter(102)); // Paint slider if (scrollBar->subControls & SC_ScrollBarSlider) { + log_debugcpp("Final scrollbar paint if"); if (transient) { QRect rect = scrollBarSlider.adjusted(horizontal ? 1 : 2, horizontal ? 2 : 1, -1, -1); painter->setPen(Qt::NoPen); @@ -535,14 +540,20 @@ public: } else { QRect pixmapRect = scrollBarSlider; painter->setPen(QPen(alphaOutline)); - if (option->state & State_Sunken && scrollBar->activeSubControls & SC_ScrollBarSlider) - painter->setBrush(midColor2); - else if (option->state & State_MouseOver && scrollBar->activeSubControls & SC_ScrollBarSlider) - painter->setBrush(highlightedGradient); - else if (!isDarkBg) - painter->setBrush(gradient); - else - painter->setBrush(midColor2); + if (option->state & State_Sunken + && scrollBar->activeSubControls & SC_ScrollBarSlider) + painter->setBrush(heldColor); + else if (option->state & State_MouseOver + && scrollBar->activeSubControls & SC_ScrollBarSlider) { + painter->setBrush(highlightColor); + } + else //if (!isDarkBg) + + if(!isDarkBg) + painter->setBrush(Qt::black); + else painter->setBrush(Qt::white); + //else + // painter->setBrush(heldColor); painter->drawRoundedRect(pixmapRect.adjusted(horizontal ? -1 : 0, horizontal ? 0 : -1, horizontal ? 0 : -1, horizontal ? -1 : 0), 5, 5); @@ -675,6 +686,13 @@ private: return gradient; } + QColor highlightedOutline(const QPalette &pal) const { + QColor highlightedOutline = highlight(pal);//.darker(25);//QColor(Qt::green); + if (highlightedOutline.value() > 160) + highlightedOutline.setHsl(highlightedOutline.hue(), highlightedOutline.saturation(), 160); + return highlightedOutline; + } + QColor calculateButtonColor(const QPalette &pal) const { QColor buttonColor = pal.button().color(); int val = qGray(buttonColor.rgb()); @@ -835,3 +853,4 @@ private: */ //void MixerStyle:: + From 8e93120555a35458a2289fc5cfb099c62a1bf8da Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 27 Nov 2024 20:46:02 +0100 Subject: [PATCH 16/38] correct size on all resolutions --- src/qt/qtclasses.cpp | 52 +++++++++++++++----------------------------- src/qt/qtclasses.h | 2 +- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 48660f1..78d99f0 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -282,31 +282,24 @@ void MainWindow::compose() { screenRes.getCoords(&srx1, &sry1, &srx2, &sry2); log_debugcpp("(for Percentage) Screen Res: " + std::to_string(srx1) + " " + std::to_string(srx2)+" " + std::to_string(sry1) + " " + std::to_string(sry2)); - dpr = screen->devicePixelRatio(); - log_to_file("dpr: %f \n", dpr); - uint64_t windowWidth = ((uint64_t)std::abs(screenRes.width()) * widthRatio) * dpr; + + uint64_t windowWidth = (((uint64_t)std::abs(screenRes.width())) * widthRatio); uint64_t screenHeight = (uint64_t)std::abs(screenRes.height()); log_debugcpp("Window Width: " + std::to_string(windowWidth)); + log_to_file("Window Width: %d \n", windowWidth); - QFontMetrics fontMetrics = this->fontMetrics(); - int maxCharWidth = fontMetrics.maxWidth(); - int charHeight = fontMetrics.height(); - //QSize QFontMetrics::size(int flags, const QString &text, int tabStops = 0, int *tabArray = nullptr) const this->createLayout(new QGridLayout()); - //scrollArea->verticalScrollBar()->setMinimumWidth(windowWidth * (widthRatio)); - //scrollArea->verticalScrollBar()->setMaximumWidth(windowWidth * (widthRatio)); - /* - * this->setAttribute(Qt::WA_DontShowOnScreen, true); - * this->show(); - * this->widget->layout()->update(); - * this->hide(); - * this->setAttribute(Qt::WA_DontShowOnScreen, false); - */ - + this->scrollArea->setMaximumWidth((int)windowWidth); + this->scrollArea->setMinimumWidth((int)windowWidth); + this->mainMenuBar->setMinimumWidth((int)windowWidth); + this->mainMenuBar->setMaximumWidth((int)windowWidth); for (auto *epw : ews) { if (!epw) continue; - epw->calculateSize(windowWidth, screenHeight); + + epw->calculateSize(windowWidth - scrollArea->verticalScrollBar()->sizeHint().width() + - widgetLayout->contentsMargins().left() + , screenHeight); log_debugcpp("epw loop"); log_debugcpp("epw roles: " + print_as_binary((epw->getEndpointHandler()->getRoles()))); //std::bitset content = @@ -324,7 +317,6 @@ void MainWindow::compose() { ews[i]->layout()->getContentsMargins(&left, &top, &right, &bottom); windowHeight += ews[i]->sizeHint().height(); windowHeight += top * 3; - //windowHeight += bottom; log_debugcpp("windowHeight loop: " + std::to_string(windowHeight)); } windowHeight += mainMenuBar->sizeHint().height(); @@ -522,16 +514,6 @@ ChannelWidget::ChannelWidget(uint32_t channelCount, EndpointHandler* eph, QWidge this->setLayout(widgetLayout); } -/* - * QSize ChannelWidget::minimumSizeHint() const { - * return minimum; - * } - * - * void ChannelWidget::setMinimum(QSize minimum) { - * this->minimum = minimum; - * } - */ - void ChannelWidget::updateChannel(int channel) { this->channelSliders.at(channel)->blockSignals(true); this->channelSliders.at(channel)->setValue((int)((eph->getCallbackInfo()->channelVolumes[channel] + roundingFactor) * 100)); @@ -553,6 +535,7 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i this->eph->setState(EndpointState::ENDPOINT_ACTIVE, idx); this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); widgetLayout = new QGridLayout(this); + //this->setContentsMargins(0, 0, 0, 0); //this->setLayout(widgetLayout); log_debugcpp("epw main layout parent: " + std::to_string((intptr_t)(widgetLayout->parent()))); if (parent == nullptr) { log_debugcpp("ayooooo?"); } @@ -796,7 +779,7 @@ void MainWindow::removeEndpointWidget(CustomWidgetEvent* ev){ } void MainWindow::addEndpointWidget(CustomWidgetEvent* ev){ - EndpointWidget* epw = new EndpointWidget(ev->payload, widget, this->ews.size()); + EndpointWidget* epw = new EndpointWidget(ev->payload, containerWidget, this->ews.size()); //epw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); //this->widgetLayout->addWidget(epw); ews.push_back(epw); @@ -952,7 +935,7 @@ void MainWindow::createLayout(QGridLayout *newLayout) { log_debugcpp("createLayout"); widgetLayout->removeItem(lastRowSpacer); delete this->widgetLayout; - widget->setLayout(newLayout); + containerWidget->setLayout(newLayout); this->widgetLayout = newLayout; bool areEndpoints = false; @@ -1011,7 +994,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { ewsUpdateTimer = new QTimer(this); recentlyClosedTimer = new QTimer(this); - widget = new QWidget(); + containerWidget = new QWidget(); + //widget->setContentsMargins(0, 0, 0, 0); widgetLayout = new QGridLayout(); trayIcon = new QSystemTrayIcon(); trayIconMenu = new QMenu(); @@ -1032,7 +1016,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { */ scrollArea = new QScrollArea(this); //widget->setAttribute(Qt::WA_TranslucentBackground); - scrollArea->setWidget(widget); + scrollArea->setWidget(containerWidget); scrollArea->setWidgetResizable(true); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -1297,7 +1281,7 @@ void MainWindow::reloadEndpointWidgets() { if (osh->getPlaybackEndpointHandlers().at(i)->getState() == EndpointState::ENDPOINT_ACTIVE){ log_debugcpp("EPWidget creation"); //osh->getPlaybackEndpointHandlers().at(i)->getCallbackInfo()->caller = osh->getGuid(); - EndpointWidget *epw = new EndpointWidget(osh->getPlaybackEndpointHandlers().at(i), widget); + EndpointWidget *epw = new EndpointWidget(osh->getPlaybackEndpointHandlers().at(i), containerWidget); log_wdebugcpp(L"epw name: " + epw->getEndpointHandler()->getName()); if ((epw->getEndpointHandler()->getRoles() == Roles::ROLE_ALL) || diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 349b2d3..36a0add 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -207,7 +207,7 @@ private: std::vector ews; std::vector> roleBucketList; - QWidget *widget; + QWidget *containerWidget; QGridLayout *widgetLayout; QSystemTrayIcon *trayIcon; From 8e07b1efddabeca3157fead56fd0165c33b705b2 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 3 Dec 2024 21:13:42 +0100 Subject: [PATCH 17/38] wip: ini parse baseline --- qtest.pro | 8 +- src/back/backsessionclasses.h | 1 - src/back/msinclude.h | 5 +- src/global.h | 11 ++ src/qt/qtclasses.cpp | 53 ++++++--- src/qt/qtclasses.h | 8 +- src/qtestmain.cpp | 14 ++- src/settings.cpp | 208 ++++++++++++++++++++++++++++++++++ src/settings.h | 75 ++++++++++++ 9 files changed, 356 insertions(+), 27 deletions(-) create mode 100644 src/settings.cpp create mode 100644 src/settings.h diff --git a/qtest.pro b/qtest.pro index ba2c13e..be04829 100644 --- a/qtest.pro +++ b/qtest.pro @@ -1,8 +1,8 @@ -QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview -O0 -Werror=return-type +QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview -O0 -Werror=return-type QMAKE_LFLAGS += --target=x86_64-w64-mingw32 -g -Wl,-pdb= -v LIBS += -LC:/capybara/libclang/x86_64-w64-mingw32/lib -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys -static -stdlib=libc++ -lunwind #"kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -DEFINES += DEBUG QT_LOGGING_TO_CONSOLE=1 WIN32_LEAN_AND_MEAN +DEFINES += DEBUG QT_LOGGING_TO_CONSOLE=1 WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 CONFIG += debug QT += widgets network @@ -10,8 +10,8 @@ INCLUDEPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimp DESTPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" -SOURCES += qtestmain.cpp qtclasses.cpp backlasses.cpp backsessionclasses.cpp contclasses.cpp contsessionclasses.cpp -HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h meterslider.h qtvisuals.h +SOURCES += qtestmain.cpp qtclasses.cpp backlasses.cpp backsessionclasses.cpp contclasses.cpp contsessionclasses.cpp settings.cpp +HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h meterslider.h qtvisuals.h settings.h RESOURCES = assets.qrc RC_ICONS += assets/logo.ico diff --git a/src/back/backsessionclasses.h b/src/back/backsessionclasses.h index 3f73f3d..ae87f0a 100644 --- a/src/back/backsessionclasses.h +++ b/src/back/backsessionclasses.h @@ -4,7 +4,6 @@ #include "global.h" #include "contclasses.h" -#include class Endpoint; class SessionStateCallback : public IAudioSessionEvents { diff --git a/src/back/msinclude.h b/src/back/msinclude.h index f9ab1b4..633e079 100644 --- a/src/back/msinclude.h +++ b/src/back/msinclude.h @@ -1,7 +1,5 @@ #pragma once -#define _WIN32_WINNT 0x0A00 - #include //done by qt by def #define UNICODE @@ -9,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -28,6 +28,7 @@ #include #include #include +#include #include "ipolicyconfig.h" #include "audiometerinfo.h" diff --git a/src/global.h b/src/global.h index 4d684a9..10f45ab 100644 --- a/src/global.h +++ b/src/global.h @@ -10,8 +10,10 @@ #include #include #include +#include #include "debug.h" +//#include "settings.h" //TODO: Use tr();? QTranslator #define STRING_MUTE "Mute" @@ -30,12 +32,17 @@ #define STRING_CP "Open Control Panel" #define STRING_ABOUT "About" #define STRING_STARTUP "Run at startup" +#define STRING_CHANNELS "Show endpoint channels" #define STRING_NOENDPOINT "No active endpoints" #define LSTRING_UNNAMED_SESSION L"Unnamed session" + + //INIT BACK + + enum ProcessedNativeEvent { NONE = 0, COLORS = (1 << 0), @@ -92,7 +99,11 @@ struct NGuid { /* }while (i < 8); */ /* } */ }; +namespace ini { + class UserSettings; +} +extern ini::UserSettings *set; class OverseerHandler; extern OverseerHandler *osh; diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 78d99f0..86c9a22 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -296,7 +296,7 @@ void MainWindow::compose() { this->mainMenuBar->setMaximumWidth((int)windowWidth); for (auto *epw : ews) { if (!epw) continue; - + epw->updateChannelsVisibility(); epw->calculateSize(windowWidth - scrollArea->verticalScrollBar()->sizeHint().width() - widgetLayout->contentsMargins().left() , screenHeight); @@ -605,8 +605,12 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i if(epChannelCount > 1) { cw = new ChannelWidget(epChannelCount, eph, nullptr); cw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - //cw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); widgetLayout->addWidget(cw, row++, 0, 1, 4 /*colmax*/, Qt::AlignTop); + cw->setVisible(false); + char* const channelSettings = set->getValue("show_channels"); + if(channelSettings && !(strcmp(channelSettings, "true"))){ + cw->setVisible(true); + } } /* @@ -889,6 +893,13 @@ void EndpointWidget::updateMainVolume(int newValue){ * } */ +void EndpointWidget::updateChannelsVisibility() { + char* const channelSettings = set->getValue("show_channels"); + if(channelSettings && !(strcmp(channelSettings, "true"))){ + cw->setVisible(true); + } else cw->setVisible(false); +} + EndpointHandler* EndpointWidget::getEndpointHandler(){ return this->eph; } @@ -915,19 +926,36 @@ std::map EndpointWidget::getDefaultRolesWidgets() { HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { widgetLayout = new QGridLayout(this); - QString text = "&" STRING_ABOUT; - about = new QPushButton(text, this); + QString text; //= "&" STRING_ABOUT; + //about = new QPushButton(text, this); #ifdef WIN32 text = "&" STRING_CP; openCP = new QPushButton(text, this); connect(openCP, &QPushButton::clicked, [](){ osh->openControlPanel(); }); - text = "&" STRING_STARTUP; - startup = new QPushButton(text, this); + + text = "&" STRING_CHANNELS; + channels = new QCheckBox(text, this); + char* const channelSettings = set->getValue("show_channels"); + if(channelSettings && !(strcmp(channelSettings, "true"))){ + channels->setChecked(true); + } + connect(channels, &QCheckBox::stateChanged, [this, parent](){ + set->setValue("show_channels", channels->isChecked(), sizeof("show_channels")); + if(parent) + QCoreApplication::instance()->postEvent + (parent, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); + }); - widgetLayout->addWidget(openCP , 0, 0); - widgetLayout->addWidget(startup, 0, 1); + + text = "&" STRING_STARTUP; + startup = new QCheckBox(text, this); + //connect(openCP, &QPushButton::clicked, [](){ osh->openControlPanel(); }); + + widgetLayout->addWidget(openCP , 0, 0, 2, 2); + widgetLayout->addWidget(channels, 0, 2, 1, 2); + widgetLayout->addWidget(startup , 1, 2, 1, 2); #endif - widgetLayout->addWidget(about , 0, 2); + //widgetLayout->addWidget(about , 0, 2); this->setLayout(widgetLayout); } @@ -1034,19 +1062,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { * Menu bar code */ mainMenuBar = new QToolBar(this); - /* - * QPalette pal; - * pal.setColor(QPalette::Window, Qt::transparent); - * mainMenuBar->setPalette(pal); - */ hw = new HeaderWidget(this); mainMenuBar->addWidget(hw); mainMenuBar->setMovable(false); this->addToolBar(Qt::BottomToolBarArea, mainMenuBar); reloadEndpointWidgets(); - //scrollArea->setMinimumWidth(ews.at(0)->minimumWidth()); - log_debugcpp(std::to_string(scrollArea->minimumWidth())); /* * Tray Icon code diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 36a0add..5531921 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -2,6 +2,7 @@ #include "qtcommon.h" #include "contclasses.h" +#include "settings.h" class MeterSlider; @@ -100,7 +101,7 @@ public: EndpointWidget(EndpointHandler* eph, QWidget *parent = nullptr, uint64_t idx = INT_MAX); //QSize minimumSizeHint() const override; //void setMinimum(uint64_t height, double heightRatio); - + void updateChannelsVisibility(); EndpointHandler* getEndpointHandler(); std::map getDefaultRolesWidgets(); @@ -167,10 +168,11 @@ public: private: QGridLayout *widgetLayout; - QPushButton *about; + //QPushButton *about; #ifdef WIN32 QPushButton *openCP; - QPushButton *startup; + QCheckBox *startup; + QCheckBox *channels; #endif }; diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index 387df74..e367ae9 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -3,9 +3,11 @@ #include "qtcommon.h" #include "qtclasses.h" #include "qtvisuals.h" +#include "settings.h" //#include "global.h" -OverseerHandler *osh = nullptr; +OverseerHandler *osh = nullptr; +ini::UserSettings *set = nullptr; QApplication* createApplication(int &argc, char *argv[]) { @@ -31,6 +33,14 @@ void closeDebugFileLog() { close_file_log_buffer(); } +char* parseCmdArgs(int argc, char* argv[]) { + if(argc == 1) return nullptr; + char arg[] = "--config-path="; + if(strstr(argv[1], arg)) { + return argv[1] + (sizeof(arg) / sizeof(arg[0])) - 1; + } else return nullptr; +} + /* set_terminate * void closeDebugFileLog2() { * close_file_log_buffer(); @@ -46,6 +56,8 @@ int main (int argc, char* argv[]) { * log_debugcpp(a.toStdString()); * } */ + //std::map* values = new std::map{ {"show_channels", false}, {"test", 7} }; + set = new ini::UserSettings(parseCmdArgs(argc, argv)); initialize_file_log(); atexit(closeDebugFileLog); diff --git a/src/settings.cpp b/src/settings.cpp new file mode 100644 index 0000000..0ced9fd --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,208 @@ +#include "settings.h" +#include "msinclude.h" + +namespace ini { + + UserSettings::UserSettings(char* path) { + wchar_t* settingsPath = nullptr; + wchar_t settingsFile[] = L"\\settings.ini"; + uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; + wchar_t maxPathBypass[] = L"\\\\?\\"; + log_wdebugcpp(L"Bypass size: " + std::to_wstring((sizeof(maxPathBypass)/sizeof(maxPathBypass[0])))); + + //Executable dir + settingsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); + uint32_t exePathLength = GetModuleFileNameW( + NULL, + settingsPath, + UNICODE_STRING_MAX_CHARS + ); + //reverse wcsstr + while(exePathLength >= 0) { + if(settingsPath[exePathLength] == '\\') { + memset(settingsPath + exePathLength, + 0, + (UNICODE_STRING_MAX_CHARS - exePathLength) * sizeof(wchar_t)); + break; + } else exePathLength--; + } + log_wdebugcpp(L"Exe folder: " + std::wstring(settingsPath)); + + HANDLE settingsHandle = nullptr; + if((UNICODE_STRING_MAX_CHARS - exePathLength) > (settingsFileLen + 1)) { + memcpy(settingsPath + exePathLength, settingsFile, sizeof(wchar_t) * settingsFileLen); + settingsHandle = CreateFile2( + settingsPath, + GENERIC_READ | GENERIC_WRITE, + 0, + OPEN_ALWAYS, + NULL); + if(settingsHandle != INVALID_HANDLE_VALUE) { log_debugcpp("Filecreated!"); } + else { CloseHandle(settingsHandle); settingsHandle = nullptr; return; } + } + + //Calculating file size and reading file + if(settingsHandle) { + uint64_t fileSize; + LARGE_INTEGER fileSizeStruct; + if(!GetFileSizeEx(settingsHandle, &fileSizeStruct)) return; + fileSize = fileSizeStruct.QuadPart; + + uint32_t bytesRead = 0; + textContentsSize = fileSize + 1; + textContents = (char*)calloc(textContentsSize, sizeof(char)); + if (ReadFile(settingsHandle, textContents, fileSize, + (LPDWORD)&bytesRead, NULL) != TRUE) { + free(textContents); + return; + } + //textContents.assign(tempTextContents); + //free(tempTextContents); + + //Parsing values + bool isCRLF = false; + char *curLine = textContents; + char *separator = nullptr, *key = nullptr, *value = nullptr; + while(curLine) { + char* nextLine = strchr(curLine, '\n'); + if(nextLine == curLine + 1 || nextLine == curLine + 2) goto nextIteration; + if (nextLine && (isCRLF || *(nextLine - 1) == '\r')) { + isCRLF = true; + *(nextLine - 1) = '\0'; + } else if (nextLine) *nextLine = '\0'; // temporarily terminate the current line + log_debugcpp("curLine: " + std::string(curLine) + " "); + + separator = strchr(curLine, '='); + if(!separator) goto nextIteration; + *separator = '\0'; + key = trimAndAllocate(curLine); + value = trimAndAllocate(separator + 1); + values.try_emplace(key, value); + log_debugcpp("ini Map size: " + std::to_string(values.size())); + *separator = '='; + + nextIteration: + if (nextLine) { // then restore newline-char, just to be tidy + if (isCRLF) + *(nextLine - 1) = '\r'; + else *nextLine = '\n'; + } + curLine = nextLine ? (nextLine + 1) : NULL; + } + /* + * for(const std::pair keyVal : defaultValues) { + * if (textContents.find(keyVal.first) { + * if (keyVal. + * } + * } + */ + } + } + + //todo: buffer overflow. poc + char* const UserSettings::getValue(char* key, uint64_t len) { + if (auto search = values.find(key); search != values.end()) + return (char* const) search->second; + return nullptr; + } + + void UserSettings::setValue(char* key, char* value, uint64_t valueSize, uint64_t keySize) { + char *newValue, *newKey; + if (auto search = values.find(key); search != values.end()) { + if(!(strcmp(value, search->second))) return; + newValue = (char*)calloc(valueSize, sizeof(char)); + if (!(search->second == pos || search->second == neg)) { + free(search->second); + } + search->second = newValue; + return; + } + + newValue = (char*)calloc(valueSize, sizeof(char)); + newKey = (char*)calloc(keySize, sizeof(char)); + values.insert(std::make_pair(newKey, newValue)); + return; + } + + void UserSettings::setValue(char* key, bool value, uint64_t keySize) { + char *newKey; + log_debugcpp("Pos value: " + std::to_string((intptr_t)pos)); + log_debugcpp("Neg value: " + std::to_string((intptr_t)neg)); + if (auto search = values.find(key); search != values.end()) { + log_debugcpp("Previous value: " + std::to_string((intptr_t)values[key])); + if (!(search->second == pos || search->second == neg)) { + free(search->second); + } + if (value) + search->second = pos; + else search->second = neg; + return; + } + + newKey = (char*)calloc(keySize, sizeof(char)); + values.insert(std::make_pair(newKey, value ? pos : neg)); + return; + } + + + + //AppData path + /* + * wchar_t folderPath[] = L"\\mixerq"; + * + * wchar_t* roamingPath = nullptr; + * if(SHGetKnownFolderPath( + * FOLDERID_RoamingAppData, + * 0, + * NULL, + * &roamingPath) + * == S_OK) { + * uint32_t pathLen = 0; + * wchar_t currentChar = roamingPath[pathLen]; + * while(currentChar != '\0') { + * pathLen++; + * currentChar = roamingPath[pathLen]; + * } + * + * uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; + * uint32_t folderPathLen = (sizeof(folderPath) / sizeof(wchar_t)) - 1; + * settingsPath = (wchar_t*)calloc(pathLen + + * maxPathBypassLen + + * folderPathLen + + * settingsFileLen, + * sizeof(wchar_t)); + * memcpy(settingsPath, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); + * memcpy(settingsPath + (maxPathBypassLen), roamingPath, sizeof(wchar_t) * pathLen); + * CoTaskMemFree(roamingPath); + * memcpy(settingsPath + (maxPathBypassLen + pathLen), + * folderPath, sizeof(wchar_t) * folderPathLen); + * log_wdebugcpp(L"Settings folder path: " + std::wstring(settingsPath)); + * + * if(CreateDirectoryW(settingsPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) { + * memcpy(settingsPath + (maxPathBypassLen + pathLen + folderPathLen), + * settingsFile, sizeof(wchar_t) * settingsFileLen); + * + * HANDLE settingsHandle = CreateFile2( + * settingsPath, + * GENERIC_READ | GENERIC_WRITE, + * 0, + * OPEN_ALWAYS, + * NULL); + * if(settingsHandle != INVALID_HANDLE_VALUE) log_debugcpp("Filecreated!"); + * + * } + * //End AppData + */ + + + +UserSettings::~UserSettings() { + if(textContents) free(textContents); + for(std::pair entry : values) { + free(entry.first); + free(entry.second); + } +} + + +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..2e88598 --- /dev/null +++ b/src/settings.h @@ -0,0 +1,75 @@ +#pragma once +#include "global.h" + +namespace ini { + + //Trims spaces, LF and CRLF + static inline char* trimAndAllocate(const char* in, uint64_t len = 0) { + if (!in) return nullptr; + + uint64_t startingPos = 0, lastPos = 0; + bool foundStart = false; + for(int i = 0; ;i++) { + char c = in[i]; + if ((len > 0 && startingPos == (len - 1)) || (len > 0 && lastPos == (len - 1))) return nullptr; + if ((c != ' ' || c != '\r' || c != '\n') && !foundStart) { + foundStart = true; + lastPos = startingPos; + } + if (foundStart && (c == ' ' || c == '\r' || c == '\n')) { + break; + } + if(!foundStart) + startingPos++; + else lastPos++; + } + if(!(lastPos - startingPos)) return nullptr; + + char* trimmedString = (char*)calloc(lastPos - startingPos + 1, sizeof(char)); + memcpy(trimmedString, in + startingPos, lastPos - startingPos); + return trimmedString; + } + + struct Djb12Hasher { + size_t operator()(char* str) const { + unsigned long hash = 5381; + int c; + + while (c = *str++) + hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ + + return hash; + } + }; + + struct StrcmpEqual { + bool operator()(char* key1, char* key2) const { + return (!strcmp(key1, key2)); + } + }; + +class UserSettings { + + public: + UserSettings(char* path = nullptr); + ~UserSettings(); + + char* const getValue(char* key, uint64_t len = 0); + void setValue(char* key, char* value, uint64_t valueSize, uint64_t keySize); //'\0' included + void setValue(char* key, bool value, uint64_t keySize); //'\0' included + //void setValue(char* key, uint64_t value, uint64_t valueSize, uint64_t keySize); //'\0' included + + protected: + + private: + //void* returnValue(); + + //std::map values{ {"show_channels", false}, {"test", 7} }; + std::unordered_map values; + char* textContents = nullptr; + uint64_t textContentsSize = 0; + char* pos = "true"; + char* neg = "false"; +}; + +} From 1ae324b68a9f64c82d6408d6c6d2c1d523ff4071 Mon Sep 17 00:00:00 2001 From: Hane Date: Fri, 6 Dec 2024 17:55:46 +0100 Subject: [PATCH 18/38] wip: settings in effect --- src/back/backlasses.cpp | 90 +++++++++++++++ src/back/backlasses.h | 36 ++++++ src/cont/contclasses.cpp | 32 +++++- src/cont/contclasses.h | 9 +- src/global.h | 6 +- src/qtestmain.cpp | 11 +- src/settings.cpp | 242 +++++++++++++++------------------------ src/settings.h | 9 +- 8 files changed, 277 insertions(+), 158 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index e51c011..adee2d6 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -1,6 +1,96 @@ #include "backlasses.h" #include "backfuncs.h" +std::string getPath(SettingsTargetDirectory target, bool create) { + wchar_t* settingsPath = nullptr; + wchar_t settingsFile[] = L"\\settings.ini"; + uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; + wchar_t maxPathBypass[] = L"\\\\?\\"; + uint32_t exePathLength = 0; + wchar_t folderPath[] = L"\\mixerq"; + uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; + uint32_t folderPathLen = (sizeof(folderPath) / sizeof(wchar_t)) - 1; + wchar_t* roamingPath = nullptr; + + log_wdebugcpp(L"Bypass size: " + std::to_wstring((sizeof(maxPathBypass)/sizeof(maxPathBypass[0])))); + + switch(target) { + case HOME_DIR: + { + if(SHGetKnownFolderPath( + FOLDERID_RoamingAppData, + 0, + NULL, + &roamingPath) + == S_OK) { + //Retrieve path len + uint32_t pathLen = 0; + wchar_t currentChar = roamingPath[pathLen]; + while(currentChar != '\0') { + pathLen++; + currentChar = roamingPath[pathLen]; + } + + + settingsPath = (wchar_t*)calloc(pathLen + + maxPathBypassLen + + folderPathLen + + settingsFileLen, + sizeof(wchar_t)); + memcpy(settingsPath, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); + memcpy(settingsPath + (maxPathBypassLen), roamingPath, sizeof(wchar_t) * pathLen); + CoTaskMemFree(roamingPath); + memcpy(settingsPath + (maxPathBypassLen + pathLen), + folderPath, sizeof(wchar_t) * folderPathLen); + log_wdebugcpp(L"Settings folder path: " + std::wstring(settingsPath)); + + if(CreateDirectoryW(settingsPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) { + memcpy(settingsPath + (maxPathBypassLen + pathLen + folderPathLen), + settingsFile, sizeof(wchar_t) * settingsFileLen); + std::string utf8path = utf16ToUtf8(settingsPath); + free(settingsPath); + return utf8path; + } + } + } + return nullptr; + break; + case APP_PATH: + { + //Executable dir + settingsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); + exePathLength = GetModuleFileNameW( + NULL, + settingsPath, + UNICODE_STRING_MAX_CHARS + ); + //reverse wcsstr + while(exePathLength >= 0) { + if(settingsPath[exePathLength] == '\\') { + memset(settingsPath + exePathLength, + 0, + (UNICODE_STRING_MAX_CHARS - exePathLength) * sizeof(wchar_t)); + break; + } else exePathLength--; + } + log_wdebugcpp(L"Exe folder: " + std::wstring(settingsPath)); + if((UNICODE_STRING_MAX_CHARS - exePathLength) > (settingsFileLen + 1)) { + memcpy(settingsPath + exePathLength, settingsFile, sizeof(wchar_t) * settingsFileLen); + std::string utf8path = utf16ToUtf8(settingsPath); + free(settingsPath); + return utf8path; + } + } + return nullptr; + break; + default: + return nullptr; + break; + } + return nullptr; + +} + EndpointNewSessionCallback::EndpointNewSessionCallback(EndpointHandler* eph){ this->eph = eph; } diff --git a/src/back/backlasses.h b/src/back/backlasses.h index f5fba61..3a94707 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -7,6 +7,42 @@ class EndpointVolumeCallback; class Session; +std::string getPath(SettingsTargetDirectory target, bool create); + +// Convert a wide UTF16LE string to an UTF8 string +static inline std::string utf16ToUtf8(const wchar_t* wstr) { + if(!wstr || wstr[0] == '\0') return std::string(); + int size_needed = WideCharToMultiByte(CP_UTF8, + 0, + wstr, + -1, + NULL, + 0, + NULL, + NULL); + std::string str(size_needed, 0); + WideCharToMultiByte(CP_UTF8, + 0, + wstr, + -1, + &str[0], + size_needed, + NULL, + NULL); + return str; +} + +// Convert an UTF8 string to a wide UTF16LE String +/* + * std::wstring utf8_decode(const std::string &str) + * { + * if( str.empty() ) return std::wstring(); + * int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), NULL, 0); + * std::wstring wstrTo( size_needed, 0 ); + * MultiByteToWideChar (CP_UTF8, 0, &str[0], (int)str.size(), &wstrTo[0], size_needed); + * return wstrTo; + * } + */ class Endpoint { diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 71f981d..c6a2149 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -1,6 +1,27 @@ #include "backlasses.h" #include "contclasses.h" -//TODO: pragma once + +void setConfigDirToDefaults() { + #define tryFileDir(dir, create) do { \ + OverseerHandler::settingsPath = getPath(dir, create); \ + set = ini::UserSettings::createSettings(OverseerHandler::settingsPath.c_str()); \ + if(set) { \ + return; \ + } else OverseerHandler::settingsPath.clear(); \ + } while(0) + #define tryOpenFileDir(dir) tryFileDir(dir, false) + #define tryCreateFileDir(dir) tryFileDir(dir, true) + + tryOpenFileDir(SettingsTargetDirectory::APP_PATH); + tryOpenFileDir(SettingsTargetDirectory::HOME_DIR); + tryCreateFileDir(SettingsTargetDirectory::HOME_DIR); + tryCreateFileDir(SettingsTargetDirectory::APP_PATH); + + return; + #undef tryOpenFileDir + #undef tryCreateFileDir + #undef tryFileDir +} EndpointHandler::EndpointHandler(uint64_t idx, Flows flow) { //std::vector endpoints = osh->getPlaybackEndpoints().at(idx); @@ -205,6 +226,14 @@ OverseerHandler::OverseerHandler() { this->os = new Overseer(); } +void OverseerHandler::setSettingsPath(std::string path) { + OverseerHandler::settingsPath = path; +} + +std::string OverseerHandler::getSettingsPath(){ + return OverseerHandler::settingsPath; +} + void OverseerHandler::populateSystemValues() { this->os->populateSystemValues(); } @@ -374,3 +403,4 @@ void OverseerHandler::setRemoveEndpointWidgetFunction(std::function ephs){ this->playbackEndpointHandlers = ephs; } + diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index e3ebab6..0f5cedd 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -1,6 +1,7 @@ #pragma once #include "global.h" +#include "settings.h" #include "contsessionclasses.h" //#define invoke_mem_fn(object,ptrToMember) ((object).*(ptrToMember)) //#define pinvoke_mem_fn(object,ptrToMember) ((object)->*(ptrToMember)) @@ -16,11 +17,13 @@ struct BackEndpointVolumeCallbackInfo { NGuid caller; bool muted; float mainVolume; - size_t channels; + size_t channels; std::vector channelVolumes; bool updateName = false; }; +void setConfigDirToDefaults(); + class EndpointHandler { public: @@ -97,6 +100,9 @@ class OverseerHandler { public: OverseerHandler(); + static void setSettingsPath(std::string path); + static std::string getSettingsPath(); + static inline std::string settingsPath; void populateSystemValues(); void openControlPanel(); ProcessedNativeEvent processTopLevelWindowMessage(void* msg); @@ -143,6 +149,7 @@ private: /* Session's */ std::function changeSessionVolume; + //std::function updateFrontVolumeCallback; //std::function updateFrontMuteCallback; diff --git a/src/global.h b/src/global.h index 10f45ab..7c02902 100644 --- a/src/global.h +++ b/src/global.h @@ -41,7 +41,11 @@ //INIT BACK - +enum SettingsTargetDirectory { + HOME_DIR = 0, + APP_PATH = (1 << 0), + CUSTOM = (1 << 1), +}; enum ProcessedNativeEvent { NONE = 0, diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index e367ae9..8b34385 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -4,7 +4,6 @@ #include "qtclasses.h" #include "qtvisuals.h" #include "settings.h" -//#include "global.h" OverseerHandler *osh = nullptr; ini::UserSettings *set = nullptr; @@ -56,8 +55,14 @@ int main (int argc, char* argv[]) { * log_debugcpp(a.toStdString()); * } */ - //std::map* values = new std::map{ {"show_channels", false}, {"test", 7} }; - set = new ini::UserSettings(parseCmdArgs(argc, argv)); + char* userSettingsPath = parseCmdArgs(argc, argv); + if (userSettingsPath) + set = ini::UserSettings::createSettings(userSettingsPath, true); + + if (set) + OverseerHandler::settingsPath = std::string(userSettingsPath); + else setConfigDirToDefaults(); + initialize_file_log(); atexit(closeDebugFileLog); diff --git a/src/settings.cpp b/src/settings.cpp index 0ced9fd..01b5114 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -3,100 +3,48 @@ namespace ini { - UserSettings::UserSettings(char* path) { - wchar_t* settingsPath = nullptr; - wchar_t settingsFile[] = L"\\settings.ini"; - uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; - wchar_t maxPathBypass[] = L"\\\\?\\"; - log_wdebugcpp(L"Bypass size: " + std::to_wstring((sizeof(maxPathBypass)/sizeof(maxPathBypass[0])))); + wchar_t* utf8toUtf16(const char* str) { + if(!str || str[0] == '\0') return nullptr; + int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + wchar_t* utf16 = (wchar_t*)calloc(sizeNeeded, 1); + MultiByteToWideChar(CP_UTF8, 0, str, -1, utf16, sizeNeeded); + return utf16; + } - //Executable dir - settingsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); - uint32_t exePathLength = GetModuleFileNameW( - NULL, - settingsPath, - UNICODE_STRING_MAX_CHARS - ); - //reverse wcsstr - while(exePathLength >= 0) { - if(settingsPath[exePathLength] == '\\') { - memset(settingsPath + exePathLength, - 0, - (UNICODE_STRING_MAX_CHARS - exePathLength) * sizeof(wchar_t)); - break; - } else exePathLength--; - } - log_wdebugcpp(L"Exe folder: " + std::wstring(settingsPath)); + UserSettings::UserSettings(char* textContents) { + //Parsing values + bool isCRLF = false; + char *curLine = textContents; + char *separator = nullptr, *key = nullptr, *value = nullptr; + while(curLine) { + char* nextLine = strchr(curLine, '\n'); + if(nextLine == curLine + 1 || nextLine == curLine + 2) + goto nextIteration; + if (nextLine && (isCRLF || *(nextLine - 1) == '\r')) { + isCRLF = true; + *(nextLine - 1) = '\0'; + } else if (nextLine) *nextLine = '\0'; // temporarily terminate the current line + log_debugcpp("curLine: " + std::string(curLine) + " "); - HANDLE settingsHandle = nullptr; - if((UNICODE_STRING_MAX_CHARS - exePathLength) > (settingsFileLen + 1)) { - memcpy(settingsPath + exePathLength, settingsFile, sizeof(wchar_t) * settingsFileLen); - settingsHandle = CreateFile2( - settingsPath, - GENERIC_READ | GENERIC_WRITE, - 0, - OPEN_ALWAYS, - NULL); - if(settingsHandle != INVALID_HANDLE_VALUE) { log_debugcpp("Filecreated!"); } - else { CloseHandle(settingsHandle); settingsHandle = nullptr; return; } - } + separator = strchr(curLine, '='); + if(!separator) + goto nextIteration; + *separator = '\0'; + key = trimAndAllocate(curLine); + value = trimAndAllocate(separator + 1); + values.try_emplace(key, value); + log_debugcpp("ini Map size: " + std::to_string(values.size())); + *separator = '='; - //Calculating file size and reading file - if(settingsHandle) { - uint64_t fileSize; - LARGE_INTEGER fileSizeStruct; - if(!GetFileSizeEx(settingsHandle, &fileSizeStruct)) return; - fileSize = fileSizeStruct.QuadPart; - - uint32_t bytesRead = 0; - textContentsSize = fileSize + 1; - textContents = (char*)calloc(textContentsSize, sizeof(char)); - if (ReadFile(settingsHandle, textContents, fileSize, - (LPDWORD)&bytesRead, NULL) != TRUE) { - free(textContents); - return; + nextIteration: + if (nextLine) { // then restore newline-char, just to be tidy + if (isCRLF) + *(nextLine - 1) = '\r'; + else *nextLine = '\n'; } - //textContents.assign(tempTextContents); - //free(tempTextContents); - - //Parsing values - bool isCRLF = false; - char *curLine = textContents; - char *separator = nullptr, *key = nullptr, *value = nullptr; - while(curLine) { - char* nextLine = strchr(curLine, '\n'); - if(nextLine == curLine + 1 || nextLine == curLine + 2) goto nextIteration; - if (nextLine && (isCRLF || *(nextLine - 1) == '\r')) { - isCRLF = true; - *(nextLine - 1) = '\0'; - } else if (nextLine) *nextLine = '\0'; // temporarily terminate the current line - log_debugcpp("curLine: " + std::string(curLine) + " "); - - separator = strchr(curLine, '='); - if(!separator) goto nextIteration; - *separator = '\0'; - key = trimAndAllocate(curLine); - value = trimAndAllocate(separator + 1); - values.try_emplace(key, value); - log_debugcpp("ini Map size: " + std::to_string(values.size())); - *separator = '='; - - nextIteration: - if (nextLine) { // then restore newline-char, just to be tidy - if (isCRLF) - *(nextLine - 1) = '\r'; - else *nextLine = '\n'; - } - curLine = nextLine ? (nextLine + 1) : NULL; - } - /* - * for(const std::pair keyVal : defaultValues) { - * if (textContents.find(keyVal.first) { - * if (keyVal. - * } - * } - */ + curLine = nextLine ? (nextLine + 1) : NULL; } + free(textContents); } //todo: buffer overflow. poc @@ -139,70 +87,68 @@ namespace ini { return; } - newKey = (char*)calloc(keySize, sizeof(char)); + newKey = (char*)calloc(keySize, sizeof(char)); values.insert(std::make_pair(newKey, value ? pos : neg)); return; } - - - //AppData path - /* - * wchar_t folderPath[] = L"\\mixerq"; - * - * wchar_t* roamingPath = nullptr; - * if(SHGetKnownFolderPath( - * FOLDERID_RoamingAppData, - * 0, - * NULL, - * &roamingPath) - * == S_OK) { - * uint32_t pathLen = 0; - * wchar_t currentChar = roamingPath[pathLen]; - * while(currentChar != '\0') { - * pathLen++; - * currentChar = roamingPath[pathLen]; - * } - * - * uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; - * uint32_t folderPathLen = (sizeof(folderPath) / sizeof(wchar_t)) - 1; - * settingsPath = (wchar_t*)calloc(pathLen + - * maxPathBypassLen + - * folderPathLen + - * settingsFileLen, - * sizeof(wchar_t)); - * memcpy(settingsPath, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); - * memcpy(settingsPath + (maxPathBypassLen), roamingPath, sizeof(wchar_t) * pathLen); - * CoTaskMemFree(roamingPath); - * memcpy(settingsPath + (maxPathBypassLen + pathLen), - * folderPath, sizeof(wchar_t) * folderPathLen); - * log_wdebugcpp(L"Settings folder path: " + std::wstring(settingsPath)); - * - * if(CreateDirectoryW(settingsPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) { - * memcpy(settingsPath + (maxPathBypassLen + pathLen + folderPathLen), - * settingsFile, sizeof(wchar_t) * settingsFileLen); - * - * HANDLE settingsHandle = CreateFile2( - * settingsPath, - * GENERIC_READ | GENERIC_WRITE, - * 0, - * OPEN_ALWAYS, - * NULL); - * if(settingsHandle != INVALID_HANDLE_VALUE) log_debugcpp("Filecreated!"); - * - * } - * //End AppData - */ - + UserSettings::~UserSettings() { + //if(textContents) free(textContents); + for(std::pair entry : values) { + free(entry.first); + if (!(entry.second == pos || entry.second == neg)) + free(entry.second); + } + } - -UserSettings::~UserSettings() { - if(textContents) free(textContents); - for(std::pair entry : values) { - free(entry.first); - free(entry.second); + UserSettings* UserSettings::createSettings(const char* path, bool create) { + if(!path) return nullptr; + wchar_t* utf16Path = utf8toUtf16(path); + if(!utf16Path) return nullptr; + + #define releaseBeforeReturn() do { \ + CloseHandle(settingsHandle); \ + settingsHandle = nullptr; \ + free(utf16Path); \ + } while(0) + + char* textContents; + HANDLE settingsHandle = nullptr; + settingsHandle = CreateFile2( + utf16Path, + GENERIC_READ | GENERIC_WRITE, + 0, + (create ? OPEN_ALWAYS : OPEN_EXISTING), + NULL); + if(settingsHandle == INVALID_HANDLE_VALUE) { + releaseBeforeReturn(); + return nullptr; + } + + //Calculating file size and reading file + uint64_t fileSize; + LARGE_INTEGER fileSizeStruct; + if(!GetFileSizeEx(settingsHandle, &fileSizeStruct)) { + releaseBeforeReturn(); + return nullptr; + } + fileSize = fileSizeStruct.QuadPart; + + uint32_t bytesRead = 0; + uint64_t textContentsSize = fileSize + 1; + textContents = (char*)calloc(textContentsSize, sizeof(char)); + if (ReadFile(settingsHandle, textContents, fileSize, + (LPDWORD)&bytesRead, NULL) != TRUE) { + releaseBeforeReturn(); + return nullptr; + } + + releaseBeforeReturn(); + return new UserSettings(textContents); + + //textContents.assign(tempTextContents); + //free(tempTextContents); + #undef releaseBeforeReturn } -} - } diff --git a/src/settings.h b/src/settings.h index 2e88598..e23a0a6 100644 --- a/src/settings.h +++ b/src/settings.h @@ -2,7 +2,7 @@ #include "global.h" namespace ini { - + //Trims spaces, LF and CRLF static inline char* trimAndAllocate(const char* in, uint64_t len = 0) { if (!in) return nullptr; @@ -51,7 +51,7 @@ namespace ini { class UserSettings { public: - UserSettings(char* path = nullptr); + static UserSettings* createSettings(const char* path = nullptr, bool create = false); ~UserSettings(); char* const getValue(char* key, uint64_t len = 0); @@ -63,11 +63,12 @@ class UserSettings { private: //void* returnValue(); + UserSettings(char* text = nullptr); //std::map values{ {"show_channels", false}, {"test", 7} }; std::unordered_map values; - char* textContents = nullptr; - uint64_t textContentsSize = 0; + //char* textContents = nullptr; + //uint64_t textContentsSize = 0; char* pos = "true"; char* neg = "false"; }; From adca5111f6c50022dad6b569696d33fdc6ea7fe2 Mon Sep 17 00:00:00 2001 From: Hane Date: Fri, 6 Dec 2024 19:59:09 +0100 Subject: [PATCH 19/38] finished basic ini skeleton --- src/back/backlasses.cpp | 1 - src/qt/qtclasses.cpp | 2 ++ src/settings.cpp | 80 +++++++++++++++++++++++++++++++++++++++-- src/settings.h | 11 ++---- 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index adee2d6..f003403 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -30,7 +30,6 @@ std::string getPath(SettingsTargetDirectory target, bool create) { pathLen++; currentChar = roamingPath[pathLen]; } - settingsPath = (wchar_t*)calloc(pathLen + maxPathBypassLen + diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 86c9a22..cecaf5a 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -941,6 +941,8 @@ HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { } connect(channels, &QCheckBox::stateChanged, [this, parent](){ set->setValue("show_channels", channels->isChecked(), sizeof("show_channels")); + if(!OverseerHandler::settingsPath.empty()) + set->save(OverseerHandler::settingsPath.c_str()); if(parent) QCoreApplication::instance()->postEvent (parent, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); diff --git a/src/settings.cpp b/src/settings.cpp index 01b5114..3760983 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -3,14 +3,17 @@ namespace ini { - wchar_t* utf8toUtf16(const char* str) { + wchar_t* utf8toUtf16(const char* str, uint64_t* size = nullptr) { if(!str || str[0] == '\0') return nullptr; int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if(size) *size = sizeNeeded; wchar_t* utf16 = (wchar_t*)calloc(sizeNeeded, 1); MultiByteToWideChar(CP_UTF8, 0, str, -1, utf16, sizeNeeded); return utf16; } + + UserSettings::UserSettings(char* textContents) { //Parsing values bool isCRLF = false; @@ -47,7 +50,6 @@ namespace ini { free(textContents); } - //todo: buffer overflow. poc char* const UserSettings::getValue(char* key, uint64_t len) { if (auto search = values.find(key); search != values.end()) return (char* const) search->second; @@ -92,6 +94,80 @@ namespace ini { return; } + bool UserSettings::save(const char* path) { + wchar_t maxPathBypass[] = L"\\\\?\\"; + uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; + + if(!path) return false; + uint64_t convertedPathSize = 0; + wchar_t* convertedPath = utf8toUtf16(path, &convertedPathSize); + if(!convertedPath) return false; + wchar_t* utf16Path = (wchar_t*)calloc(maxPathBypassLen + convertedPathSize, sizeof(wchar_t)); + memcpy(utf16Path, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); + memcpy(utf16Path + maxPathBypassLen, convertedPath, sizeof(wchar_t) * convertedPathSize); + free(convertedPath); + + #define releaseBeforeReturn() do { \ + CloseHandle(settingsHandle); \ + settingsHandle = nullptr; \ + free(utf16Path); \ + free(text); \ + } while(0) + + //We initially reserve 1024B for flushing. If storage is exceeded, more same-size chunks are allocated + const uint64_t chunkSize = 1024; + char* text = (char*)calloc(chunkSize, sizeof(char)); + uint64_t mapSize = values.size(); + uint64_t chunks = 1; + uint64_t keySize = 0, valueSize = 0, totalSize = 0, previousStepSize = 0; + //for(std::pair entry : values) { + std::unordered_map::iterator it; + for (it = values.begin(); it != values.end(); it++) { + keySize = strlen(it->first); + valueSize = strlen(it->second); + totalSize += valueSize + keySize + (it == values.begin() ? 1 : 2); //newline and separator + + if(totalSize > (chunkSize * chunks)) { + text = (char*)realloc(text, (++chunks * chunkSize)); + } + + if(it != values.begin()) + memcpy(text + previousStepSize++, "\n", sizeof(char)); + memcpy(text + previousStepSize, it->first, sizeof(char) * keySize); + memcpy(text + previousStepSize + keySize, "=", sizeof(char)); + memcpy(text + previousStepSize + 1 + keySize, it->second, sizeof(char) * valueSize); + + previousStepSize = totalSize; + } + + HANDLE settingsHandle = nullptr; + settingsHandle = CreateFile2( + utf16Path, + GENERIC_READ | GENERIC_WRITE, + 0, + CREATE_ALWAYS, + NULL); + if(settingsHandle == INVALID_HANDLE_VALUE) { + releaseBeforeReturn(); + return false; + } + + DWORD bytesWritten; + BOOL writeSuccess = WriteFile( + settingsHandle, + text, + totalSize, + &bytesWritten, + nullptr + ); + + releaseBeforeReturn(); + if (writeSuccess == TRUE) return true; + else return false; + + return false; + } + UserSettings::~UserSettings() { //if(textContents) free(textContents); for(std::pair entry : values) { diff --git a/src/settings.h b/src/settings.h index e23a0a6..9974507 100644 --- a/src/settings.h +++ b/src/settings.h @@ -57,18 +57,13 @@ class UserSettings { char* const getValue(char* key, uint64_t len = 0); void setValue(char* key, char* value, uint64_t valueSize, uint64_t keySize); //'\0' included void setValue(char* key, bool value, uint64_t keySize); //'\0' included - //void setValue(char* key, uint64_t value, uint64_t valueSize, uint64_t keySize); //'\0' included - + + bool save(const char* path); protected: private: - //void* returnValue(); - UserSettings(char* text = nullptr); - - //std::map values{ {"show_channels", false}, {"test", 7} }; + UserSettings(char* text = nullptr); std::unordered_map values; - //char* textContents = nullptr; - //uint64_t textContentsSize = 0; char* pos = "true"; char* neg = "false"; }; From 9f7e7e30e28b902f94651ec6ba785a075d6e82c9 Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 12 Dec 2024 20:29:13 +0100 Subject: [PATCH 20/38] ini: fixed insufficient utf8->16 alloc --- src/qt/qtclasses.cpp | 12 ++++++++---- src/settings.cpp | 11 ++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index cecaf5a..e7d7b35 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -941,11 +941,15 @@ HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { } connect(channels, &QCheckBox::stateChanged, [this, parent](){ set->setValue("show_channels", channels->isChecked(), sizeof("show_channels")); - if(!OverseerHandler::settingsPath.empty()) + if(!OverseerHandler::settingsPath.empty()){ set->save(OverseerHandler::settingsPath.c_str()); - if(parent) - QCoreApplication::instance()->postEvent - (parent, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); + } + if(parent) { + QEvent explosion = QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow); + QCoreApplication::instance()->sendEvent + (parent, &explosion); + } + }); diff --git a/src/settings.cpp b/src/settings.cpp index 3760983..b45a9f1 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -3,11 +3,11 @@ namespace ini { - wchar_t* utf8toUtf16(const char* str, uint64_t* size = nullptr) { + wchar_t* Utf8toUtf16(const char* str, uint64_t* size = nullptr) { if(!str || str[0] == '\0') return nullptr; int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); if(size) *size = sizeNeeded; - wchar_t* utf16 = (wchar_t*)calloc(sizeNeeded, 1); + wchar_t* utf16 = (wchar_t*)calloc(sizeNeeded, sizeof(wchar_t)); MultiByteToWideChar(CP_UTF8, 0, str, -1, utf16, sizeNeeded); return utf16; } @@ -100,7 +100,7 @@ namespace ini { if(!path) return false; uint64_t convertedPathSize = 0; - wchar_t* convertedPath = utf8toUtf16(path, &convertedPathSize); + wchar_t* convertedPath = Utf8toUtf16(path, &convertedPathSize); if(!convertedPath) return false; wchar_t* utf16Path = (wchar_t*)calloc(maxPathBypassLen + convertedPathSize, sizeof(wchar_t)); memcpy(utf16Path, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); @@ -164,8 +164,9 @@ namespace ini { releaseBeforeReturn(); if (writeSuccess == TRUE) return true; else return false; - + return false; + #undef releaseBeforeReturn } UserSettings::~UserSettings() { @@ -179,7 +180,7 @@ namespace ini { UserSettings* UserSettings::createSettings(const char* path, bool create) { if(!path) return nullptr; - wchar_t* utf16Path = utf8toUtf16(path); + wchar_t* utf16Path = Utf8toUtf16(path); if(!utf16Path) return nullptr; #define releaseBeforeReturn() do { \ From 2a1b30e1666dfebe6eb06877b654a172d3fe23f1 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 17 Dec 2024 00:01:02 +0100 Subject: [PATCH 21/38] fix: sessionmanager life expired under my feet --- src/back/backlasses.cpp | 32 +++++++++++++++++++++++--------- src/back/backlasses.h | 6 +++--- src/cont/contclasses.cpp | 31 ++++++++++++++++++++++--------- src/cont/contclasses.h | 2 ++ src/qt/qtclasses.cpp | 7 +++++++ src/qt/qtclasses.h | 2 +- 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index f003403..775f929 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -351,16 +351,12 @@ Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ endpoint->OpenPropertyStore(STGM_READ, &properties); this->updateName(); this->setFlow(); - if (this->flow == Flows::FLOW_PLAYBACK) { - activateEndpointSessions(); - log_debugcpp("plays back"); - } } void Endpoint::updateName() { PROPVARIANT pv; #define store_name(key, propvariant, wstr) do { \ - properties->GetValue(key , &propvariant); \ + properties->GetValue(key, &propvariant); \ if (pv.pwszVal == nullptr) wstr = L"Unnamed Not Present Endpoint"; \ else wstr = std::wstring(pv.pwszVal); \ } while (0) @@ -373,8 +369,16 @@ void Endpoint::updateName() { } void Endpoint::activateEndpointSessions() { - //sessionManager; - if (FAILED(endpoint->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL, NULL, (void**) &sessionManager))) { log_wdebugcpp(L"sesionbros..."); return; } + if (this->flow != Flows::FLOW_PLAYBACK) { + log_debugcpp("recording. No seshes for u :("); + return; + } + + if (FAILED(endpoint->Activate(__uuidof(IAudioSessionManager2), + CLSCTX_ALL, NULL, (void**) &sessionManager))) { + log_debugcpp("Couldn't open session manager2, huh"); + return; + } IAudioSessionEnumerator* sessionEnumerator = nullptr; if (FAILED(sessionManager->GetSessionEnumerator(&sessionEnumerator))) { log_wdebugcpp(L"sesEnumeratorBros..."); return; } @@ -382,7 +386,7 @@ void Endpoint::activateEndpointSessions() { endpointSessions.resize(1, nullptr); int sessionCount; sessionEnumerator->GetCount(&sessionCount); - for (int i = 0; i < sessionCount; i++) { + for (int i = 0; i < sessionCount; i++) { IAudioSessionControl* sessionControlTmp; sessionEnumerator->GetSession(i, (IAudioSessionControl**)&sessionControlTmp); /*todo: borrar when donezo @@ -390,7 +394,7 @@ void Endpoint::activateEndpointSessions() { * IAudioMeterInformation* ttmp = (IAudioMeterInformation*)sessionControlTmp; * ttmp->GetPeakValue(&test2); */ - //todo:: asegurar lo del dynamic_cast + IAudioSessionControl2* sessionControl; sessionControlTmp->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl); sessionControl->AddRef(); @@ -593,12 +597,22 @@ void Endpoint::unregisterNewSessionNotification(EndpointNewSessionCallback* ensc sessionManager->UnregisterSessionNotification(ensc); } +void Endpoint::deleteSessions() { + for (auto session : endpointSessions) { + delete session; + } + endpointSessions.resize(0); +} + Endpoint::~Endpoint(){ log_wdebugcpp(L"murio endpoint-san uwu"); properties->Release(); endpointVolume->Release(); endpoint->Release(); sessionManager->Release(); + for (auto session : endpointSessions) { + delete session; + } } void Overseer::initCOMLibrary() { diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 3a94707..4e67a09 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -80,19 +80,19 @@ class Endpoint { void addSession(Session* session); void registerNewSessionNotification(EndpointNewSessionCallback* ensc); void unregisterNewSessionNotification(EndpointNewSessionCallback* ensc); - + void deleteSessions(); + void activateEndpointSessions(); ~Endpoint(); private: void inline activateEndpointVolume(); - void inline activateEndpointSessions(); std::vector endpointSessions; uint32_t channelCount = 0; IMMDevice *endpoint; IAudioClient *audioClient; int64_t defTime, minTime; - IAudioSessionManager2 *sessionManager; + IAudioSessionManager2 *sessionManager = nullptr; Flows flow; IAudioEndpointVolume *endpointVolume = nullptr; IPropertyStore *properties; diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index c6a2149..067a32f 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -30,19 +30,10 @@ EndpointHandler::EndpointHandler(uint64_t idx, Flows flow) { this->ep = (flow == Flows::FLOW_PLAYBACK ? osh->getPlaybackEndpoints().at(idx) : osh->getCaptureEndpoints().at(idx)); epc = new EndpointVolumeCallback(ep); - ensc = new EndpointNewSessionCallback(this); this->callbackInfo.caller = osh->getGuid(); - ep->registerNewSessionNotification(ensc); //epName = ep->getName(); this->setBackEndpointVolumeCallbackInfoContent(this->getState()); osh->pushBackEndpointHandler(this, flow); - - if (this->flow == Flows::FLOW_PLAYBACK) { - for (int i = 0; i < this->getSessionCount(); i++) { - SessionHandler* sessionHandler = new SessionHandler(this, this->getSessions().at(i),i); - sessionHandlers.push_back(sessionHandler); - } - } } void OverseerHandler::pushBackEndpointHandler(EndpointHandler* eph, Flows flow) { @@ -215,6 +206,28 @@ void EndpointHandler::removeSessionFromFront(SessionHandler* sh) { this->removeSessionWidget(sh); } +void EndpointHandler::deleteSessions() { + ep->unregisterNewSessionNotification(ensc); + ensc->Release(); + for (auto sh : sessionHandlers) { + delete sh; + } + sessionHandlers.resize(0); + ep->deleteSessions(); +} + +void EndpointHandler::createSessionHandlers() { + ep->activateEndpointSessions(); + ensc = new EndpointNewSessionCallback(this); + ep->registerNewSessionNotification(ensc); + if (this->flow == Flows::FLOW_PLAYBACK) { + for (int i = 0; i < this->getSessionCount(); i++) { + SessionHandler* sessionHandler = new SessionHandler(this, this->getSessions().at(i),i); + sessionHandlers.push_back(sessionHandler); + } + } +} + EndpointHandler::~EndpointHandler() { ep->removeVolumeCallback(epc); ep->unregisterNewSessionNotification(ensc); diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index 0f5cedd..2a4a9f0 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -74,6 +74,8 @@ public: void setRemoveSessionWidgetFunction(std::function removeSessionWidget); void sendSessionToFront(SessionHandler* sh); void removeSessionFromFront(SessionHandler* sh); + void deleteSessions(); + void createSessionHandlers(); ~EndpointHandler(); private: diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index e7d7b35..c7b5cc7 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -531,6 +531,7 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i this->idx = idx; this->eph = eph; + eph->createSessionHandlers(); //todo: sussy this->eph->setState(EndpointState::ENDPOINT_ACTIVE, idx); this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); @@ -749,6 +750,10 @@ EndpointWidget::~EndpointWidget() { timer->stop(); delete timer; this->eph->setFrontVisibilityInfo(EndpointState::ENDPOINT_ALL, INT_MAX); + this->eph->deleteSessions(); + for(auto sw : sessionWidgets) { + delete sw; + } } void MainWindow::customEvent(QEvent* ev) { @@ -894,6 +899,8 @@ void EndpointWidget::updateMainVolume(int newValue){ */ void EndpointWidget::updateChannelsVisibility() { + if (!cw) return; + char* const channelSettings = set->getValue("show_channels"); if(channelSettings && !(strcmp(channelSettings, "true"))){ cw->setVisible(true); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 5531921..1ca937d 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -149,7 +149,7 @@ private: size_t defaultRolesVectorSize = 4; QTimer* timer = nullptr; uint64_t idx; - ChannelWidget* cw; + ChannelWidget* cw = nullptr; std::vector sessionWidgets; QSize minimum; //std::vector *ephs; From 5e5365274caaef3d6e4fe889bfc83717d2349845 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 17 Dec 2024 23:00:44 +0100 Subject: [PATCH 22/38] changed session module name retrieval + name when wnd not shown --- src/back/backsessionclasses.cpp | 47 ++++++++++++--------------------- src/back/msinclude.h | 1 + 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index 1bfc37f..aafe147 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -129,12 +129,15 @@ Session::Session(Endpoint* ep, IAudioSessionControl2* sessionControl, size_t idx if (!wcscmp(sessionDisplayName, L"")) { std::wstring exePath; if (getExePath(pid, &exePath)) { + this->sessionName = exePath; if (fetchName(exePath, pid)) goto nameFound; } if (fetchNameViaWindowName(pid, &this->sessionName)) goto nameFound; - } else + } else { this->sessionName = std::wstring(sessionDisplayName); - + goto nameFound; + } + nameFound: CoTaskMemFree(sessionDisplayName); } @@ -199,37 +202,21 @@ void Session::setMute(NGuid guid, bool muted) { } bool Session::getExePath(DWORD pid, std::wstring *exePath) { - /* - * https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot - * https://stackoverflow.com/questions/11843368/how-to-get-process-description - * https://notes.indezine.com/2018/05/microsoft-locale-ids.html#:~:text=Wait%2C%201033%20is%20the%20decimal,ID%20for%20English%20%E2%80%93%20United%20States. - * https://stackoverflow.com/questions/64321036/c-win32-getting-app-name-using-pid-and-executable-path - */ - //std::wstring msixName; - - HANDLE processList = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); - if (processList == INVALID_HANDLE_VALUE) { + HANDLE processHandle; + wchar_t fileName[UNICODE_STRING_MAX_CHARS]; + processHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + if (processHandle != NULL) { + if (GetModuleFileNameEx(processHandle, NULL, fileName, UNICODE_STRING_MAX_CHARS) == 0) { + CloseHandle(processHandle); + return false; + } + } else { log_wdebugcpp(L"aye no procname. -> " + std::to_wstring(GetLastError())); return false; } - MODULEENTRY32W me32w; - me32w.dwSize = sizeof(MODULEENTRY32W); - if(Module32FirstW(processList, &me32w)) { - do { - if (me32w.th32ProcessID == pid) { - *exePath = std::wstring(me32w.szExePath); - break; - /* - * However, if the calling process is a 32-bit process, you must call the - * QueryFullProcessImageName function to retrieve the full path of the - * executable file for a 64-bit process. - */ - } - } while(Module32NextW(processList, &me32w)); - } - CloseHandle(processList); + *exePath = std::wstring(fileName); return true; } @@ -362,8 +349,8 @@ bool Session::fetchNameViaWindowName(DWORD pid, std::wstring *sessionName) { auto pParams = (std::pair*)(lParam); DWORD processId; - //&& GetWindow(hwnd, GW_OWNER) == 0 - if (IsWindowVisible(hwnd) && GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { + //IsWindowVisible(hwnd) &&&& GetWindow(hwnd, GW_OWNER) == 0 + if ( GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second) { int length = GetWindowTextLength(hwnd); if (!length) return TRUE; diff --git a/src/back/msinclude.h b/src/back/msinclude.h index 633e079..52fccbd 100644 --- a/src/back/msinclude.h +++ b/src/back/msinclude.h @@ -17,6 +17,7 @@ #include #include #include +#include //#include #include From 801adbb17ee97055ac09fc5241a82b4c3335c217 Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 18 Dec 2024 00:44:33 +0100 Subject: [PATCH 23/38] fixed 1px gaps/removed unnecesary com call --- src/back/backlasses.cpp | 16 +++------------- src/qt/qtclasses.cpp | 11 ++++++----- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 775f929..cf5700b 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -128,7 +128,6 @@ HRESULT EndpointNewSessionCallback::QueryInterface(REFIID riid, VOID **ppvInterf HRESULT EndpointNewSessionCallback::OnSessionCreated(IAudioSessionControl *NewSession) { if (eph->getFlow() == Flows::FLOW_CAPTURE) return S_OK; - HRESULT result = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); IAudioSessionControl2* sessionControl; //ISimmpleAudioVolume* sessionVolume; if (FAILED(NewSession->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl))) { log_wdebugcpp(L"no nueva sesion......"); }; @@ -139,9 +138,6 @@ HRESULT EndpointNewSessionCallback::OnSessionCreated(IAudioSessionControl *NewSe eph->addSessionSendFront(newSession); } - if (result == S_OK) - CoUninitialize(); - return S_OK; } @@ -296,7 +292,7 @@ HRESULT EndpointSituationCallback::OnDeviceRemoved(LPCWSTR pwstrDeviceId) { HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) { std::wstring endpointId = std::wstring(pwstrDeviceId); - switch (dwNewState){ + switch (dwNewState) { case DEVICE_STATE_ACTIVE: osh->reviseEndpointShowing(endpointId, EndpointState::ENDPOINT_ACTIVE); break; @@ -389,12 +385,6 @@ void Endpoint::activateEndpointSessions() { for (int i = 0; i < sessionCount; i++) { IAudioSessionControl* sessionControlTmp; sessionEnumerator->GetSession(i, (IAudioSessionControl**)&sessionControlTmp); - /*todo: borrar when donezo - * float test2; - * IAudioMeterInformation* ttmp = (IAudioMeterInformation*)sessionControlTmp; - * ttmp->GetPeakValue(&test2); - */ - IAudioSessionControl2* sessionControl; sessionControlTmp->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl); sessionControl->AddRef(); @@ -566,7 +556,6 @@ void Endpoint::removeRoles(Roles role){ void Endpoint::setFlow() { IMMEndpoint* flowGetter; - //this should be as simple as writing IID_IMMEndpoint, but it just won't find the macro, so I reimpl it. Sad. if(FAILED(this->endpoint->QueryInterface(__uuidof(IMMEndpoint), (void**)&flowGetter))) { log_debugcpp("no flow..."); } EDataFlow MSflow; @@ -790,7 +779,8 @@ ProcessedNativeEvent Overseer::processTopLevelWindowMessage(void* msg) { MSG *message = static_cast(msg); switch(message->message) { case WM_SETTINGCHANGE: - if(!wcscmp(((wchar_t*)message->lParam), L"ImmersiveColorSet")) + //TODO: This looks like a future pain point. Ex handler would come in clutch + if(message->lParam && !wcscmp(((wchar_t*)message->lParam), L"ImmersiveColorSet")) return updateColors(); break; default: diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index c7b5cc7..15f3768 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -225,7 +225,7 @@ QRect MainWindow::setSizePosition(QScreen* screen, int width, int height) { QRect availableRes = screen->availableGeometry(); int arx1, arx2, ary1, ary2; availableRes.getCoords(&arx1, &ary1, &arx2, &ary2); - log_debugcpp("Available res: " + std::to_string(arx1) + " " + std::to_string(arx2)+" " + std::to_string(ary1) + " " + std::to_string(ary2)); + log_debugcpp("Available res: " + std::to_string(arx1) + " " + std::to_string(arx2)+" " + std::to_string(ary1) + " " + std::to_string(ary2);) if(height > (ary2 - ary1)) height = (ary2 - ary1); log_debugcpp("Height res: " + std::to_string(height)); @@ -235,22 +235,22 @@ QRect MainWindow::setSizePosition(QScreen* screen, int width, int height) { int rightDistance = std::abs(srx2 - tix1); int upDistance = std::abs(sry1 - tiy1); int downDistance = std::abs(sry2 - tiy1); - + pos = (upDistance < downDistance) ? SpawnPos::UP : SpawnPos::DOWN; pos = (leftDistance < rightDistance) ? pos | SpawnPos::LEFT : pos | SpawnPos::RIGHT; switch (pos) { case SpawnPos::UP | SpawnPos::RIGHT: this->addToolBar(Qt::BottomToolBarArea, mainMenuBar); - return QRect((arx2 - width), ary1, width, height); + return QRect((arx2 - width + 1), ary1, width, height); break; case SpawnPos::DOWN | SpawnPos::LEFT: this->addToolBar(Qt::TopToolBarArea, mainMenuBar); - return QRect(arx1, (ary2-height), width, height); + return QRect(arx1, (ary2 - height + 1), width, height); break; case SpawnPos::DOWN | SpawnPos::RIGHT: this->addToolBar(Qt::TopToolBarArea, mainMenuBar); - return QRect((arx2 - width), (ary2-height), width, height); + return QRect((arx2 - width + 1), (ary2 - height + 1), width, height); break; default: this->addToolBar(Qt::BottomToolBarArea, mainMenuBar); @@ -414,6 +414,7 @@ SessionWidget::SessionWidget(uint64_t idx, SessionHandler* sh, QWidget *parent) mainSlider->setValue((int)((sh->getVolumeInfo()->mainVolume + roundingFactor) * 100)); mainSlider->setPeakValue(sh->getPeakVolume()); mainSlider->update(); + //log_wdebugcpp(L"Session: " + sh->getName() + L" peak value: " + std::to_wstring(sh->getPeakVolume())); muteButton->setCheckState((sh->getVolumeInfo()->muted == false ? Qt::Unchecked : Qt::Checked)); muteButton->setText(sh->getVolumeInfo()->muted ? STRING_UNMUTE : STRING_MUTE); //memcpy(osh->callbackInfo[idx]->caller, osh->getGuid(), sizeof(NGuid)); From 94d2ebf1c2d979a08a4a0445b99faa073f6109d3 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 24 Dec 2024 00:29:23 +0100 Subject: [PATCH 24/38] wip: working installer. cleanup/bindir/logo missing --- .gitignore | 1 + install/installer.nsi | 210 ++++++++++++++++++++++++++++++++++++++++++ install/version.nsi | 22 +++++ 3 files changed, 233 insertions(+) create mode 100644 install/installer.nsi create mode 100644 install/version.nsi diff --git a/.gitignore b/.gitignore index 9eec94c..a4ce7f0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build *.rdbg *.pdb *.ps1 +*.exe Makefile Makefile.Debug Makefile.Release \ No newline at end of file diff --git a/install/installer.nsi b/install/installer.nsi new file mode 100644 index 0000000..b2e29eb --- /dev/null +++ b/install/installer.nsi @@ -0,0 +1,210 @@ +;Auto versioning------------------------------- + +!makensis "version.nsi" +!system "GetVersion.exe" +!include "Version.txt" +; optional cleanup +!delfile "GetVersion.exe" +!delfile "Version.txt" + +;Defines--------------- +;!define MULTIUSER_EXECUTIONLEVEL Highest + + +;Includes-------------------------------- + + !include "MUI2.nsh" + !include "nsDialogs.nsh" + !include "LogicLib.nsh" + ;!include "MultiUser.nsh" + +;-------------------------------- +;General + + ;Name and file + Name "MixerQ" + OutFile "mixerq-installer-${version}.exe" + Unicode True + + ;Default installation folder + ;InstallDir "$LOCALAPPDATA\Modern UI Test" + + ;Get installation folder from registry if available + ;InstallDirRegKey HKCU "Software\Modern UI Test" "" + + Var Is_Admin + Var Install_Type + + ;Request application privileges for UAC. If admin is not available, only user-level install will be available + RequestExecutionLevel highest + + !macro ONINIT un + Function ${un}.onInit + ; The value of SetShellVarContext detetmines whether SHCTX is HKLM or HKCU + ; and whether SMPROGRAMS refers to all users or just the current user + UserInfo::GetAccountType + Pop $0 + ${If} $0 == "Admin" + ; If we're an admin, default to installing to C:\Program Files + ;SetShellVarContext all + ;StrCpy $INSTDIR_BASE "$PROGRAMFILES64" + StrCpy $Is_Admin "true" + StrCpy $Install_Type "machine" + ${Else} + ; If we're just a user, default to installing to ~\AppData\Local + ;SetShellVarContext current + ;StrCpy $INSTDIR_BASE "$LOCALAPPDATA" + StrCpy $Is_Admin "false" + StrCpy $Install_Type "user" + ${EndIf} + + ; ${If} $INSTDIR == "" + ; ; This only happens in the installer, because the uninstaller already knows INSTDIR + ; ReadRegStr $0 SHCTX "Software\${PRODUCT_NAME}" "" + + ; ${If} $0 != "" + ; ; If we're already installed, use the existing directory + ; StrCpy $INSTDIR "$0" + ; ${Else} + ; StrCpy $INSTDIR "$INSTDIR_BASE\${PRODUCT_NAME}" + ; ${Endif} + ; ${Endif} + FunctionEnd + !macroend + +!insertmacro ONINIT "" +!insertmacro ONINIT "un" + +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + +;-------------------------------- +;Pages + + !insertmacro MUI_PAGE_WELCOME + !insertmacro MUI_PAGE_LICENSE "..\LICENSE.txt" + ;!insertmacro MULTIUSER_PAGE_INSTALLMODE + Page Custom InstallTargetPage + ;!insertmacro MUI_PAGE_COMPONENTS + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_PAGE_FINISH + + !insertmacro MUI_UNPAGE_WELCOME + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + !insertmacro MUI_UNPAGE_FINISH + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;NSDialog InstallTarget Page definition--------------------------------- + +Function InstallTargetPage + !insertmacro MUI_HEADER_TEXT "Configure Install" "Customize install settings" + + nsDialogs::Create 1018 + Pop $0 + + ${NSD_CreateLabel} 0 0 100% 10% "Select for whom will $(^Name) be installed: " + Pop $3 + + ${NSD_CreateFirstRadioButton} 0 12% 40% 6% "All users" + Pop $1 + ${If} $Is_Admin == "false" + EnableWindow $1 0 + StrCpy $INSTDIR "$LOCALAPPDATA\$(^Name)" + ${Else} + SendMessage $1 ${BM_CLICK} "" "" ;Set default + StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" + ${EndIf} + ${NSD_OnClick} $1 All_Users_Click + + ${NSD_CreateAdditionalRadioButton} 0 24% 40% 6% "Current user" + Pop $2 + ${IfThen} $Is_Admin == "false" ${|} SendMessage $2 ${BM_CLICK} "" "" ${|} + ${NSD_OnClick} $2 Current_User_Click + + nsDialogs::Show +FunctionEnd + +Function All_Users_Click + Pop $0 + StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" + StrCpy $Install_Type "machine" + ;${NSD_SetText} $0 "machine" +FunctionEnd + +Function Current_User_Click + Pop $0 + StrCpy $INSTDIR "$LOCALAPPDATA\$(^Name)" + StrCpy $Install_Type "user" + ;${NSD_SetText} $0 "user" +FunctionEnd + +;Default section---------------------- +Section + SetRegView 64 + SetOutPath $INSTDIR + + ;File "..\build\debug\qtest.exe" + File "..\LICENSE.txt" + + ;Store installation folder + + ${If} $Install_Type == "user" + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "DisplayName" "$(^Name)" + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "UninstallString" '"$INSTDIR\Uninstall.exe"' + WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoModify" 1 + WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoRepair" 1 + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" "$INSTDIR\$(^Name)" + ${Else} + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "DisplayName" "$(^Name)" + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "UninstallString" '"$INSTDIR\Uninstall.exe"' + WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoModify" 1 + WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoRepair" 1 + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" "$INSTDIR\$(^Name)" + ${EndIf} + + ;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + + +;-------------------------------- +;Descriptions + + ; ;Language strings + ; LangString DESC_SecDummy ${LANG_ENGLISH} "A test section." + + ; ;Assign language strings to sections + ; !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + ; !insertmacro MUI_DESCRIPTION_TEXT ${SecDummy} $(DESC_SecDummy) + ; !insertmacro MUI_FUNCTION_DESCRIPTION_END + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + SetRegView 64 + ;ADD YOUR OWN FILES HERE... + Delete "$INSTDIR\qtest.exe" + Delete "$INSTDIR\LICENSE.txt" + Delete "$INSTDIR\Uninstall.exe" +;!define PRODUCT_UNINST_ROOT_KEY "HKLM" + RMDir "$INSTDIR" + + ${If} $Install_Type == "user" + DeleteRegValue HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" + DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" + ${Else} + DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" + DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" + ${EndIf} + +SectionEnd diff --git a/install/version.nsi b/install/version.nsi new file mode 100644 index 0000000..6f5ac83 --- /dev/null +++ b/install/version.nsi @@ -0,0 +1,22 @@ +!define File "..\build\debug\qtest.exe" + +OutFile "GetVersion.exe" +SilentInstall silent +RequestExecutionLevel user ; don't write $EXEDIR\Version.txt with admin permissions and prevent invoking UAC + +Section + + ## Get file version + GetDllVersion "${File}" $R0 $R1 + IntOp $R2 $R0 / 0x00010000 + IntOp $R3 $R0 & 0x0000FFFF + IntOp $R4 $R1 / 0x00010000 + IntOp $R5 $R1 & 0x0000FFFF + StrCpy $R1 "$R2.$R3.$R4.$R5" + + ## Write it to a !define for use in main script + FileOpen $R0 "$EXEDIR\Version.txt" w + FileWrite $R0 '!define version "$R1"' + FileClose $R0 + +SectionEnd \ No newline at end of file From 047808c89fcdb35eecb431e57c1beabd52de6c42 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 7 Jan 2025 00:00:50 +0100 Subject: [PATCH 25/38] ref Env namespace for win32/reg stuff / startup toggle Bigger than usual commit, but all of this had to be done simultaneously --- src/back/backlasses.cpp | 214 ++++++++++++++++++++++++++++++--------- src/back/backlasses.h | 34 +++++-- src/cont/contclasses.cpp | 24 +++-- src/cont/contclasses.h | 3 + src/debug.h | 5 +- src/global.h | 4 +- src/qt/qtclasses.cpp | 11 +- src/qtestmain.cpp | 99 ++++++++++++------ 8 files changed, 292 insertions(+), 102 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index cf5700b..e0238c7 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -1,7 +1,19 @@ #include "backlasses.h" #include "backfuncs.h" -std::string getPath(SettingsTargetDirectory target, bool create) { +using namespace Environment; + +wchar_t* getExeAbsPath(uint32_t *exeAbsPathLength) { + wchar_t *exeAbsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); + *exeAbsPathLength = GetModuleFileNameW( + NULL, + exeAbsPath, + UNICODE_STRING_MAX_CHARS + ); + return exeAbsPath; +} + +std::string getSettingsPath(SettingsTargetDirectory target, bool create) { wchar_t* settingsPath = nullptr; wchar_t settingsFile[] = L"\\settings.ini"; uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; @@ -57,12 +69,8 @@ std::string getPath(SettingsTargetDirectory target, bool create) { case APP_PATH: { //Executable dir - settingsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); - exePathLength = GetModuleFileNameW( - NULL, - settingsPath, - UNICODE_STRING_MAX_CHARS - ); + settingsPath = getExeAbsPath(&exePathLength); + //reverse wcsstr while(exePathLength >= 0) { if(settingsPath[exePathLength] == '\\') { @@ -730,8 +738,23 @@ Endpoint* Overseer::addEndpoint(std::wstring endpointId, /* out */Flows* flow = } Overseer::Overseer() : epsc(this){ - //Initializing COM library log_debugcpp("Initializing Overseer"); + + //Storing exe path for later use (mainly "Run on startup") + log_debugcpp("-Caching exe path"); + uint32_t cPathLen = 0; + wchar_t* cPath = getExeAbsPath(&cPathLen); + exeAbsPath = cPath; + + //Detecting install scope + wchar_t *machine = wcsstr(cPath, L"Program Files"); + if (!machine) + Environment::scope = HKEY_CURRENT_USER; + else Environment::scope = HKEY_LOCAL_MACHINE; + free(cPath); + + //Initializing COM library + log_debugcpp("-Initializing COM"); initCOMLibrary(); //Obtaining playback endpoint collection on this point in time @@ -744,11 +767,44 @@ Overseer::Overseer() : epsc(this){ if(FAILED(deviceEnumerator->RegisterEndpointNotificationCallback(((IMMNotificationClient*)&epsc)))) { log_debugcpp("when no enchufas......"); } } -void Overseer::populateSystemValues() { - updateColors(); +NGuid Overseer::getGuid() { + return guid; } -void Overseer::openControlPanel() { +std::vector Overseer::getPlaybackEndpoints() { + return playbackDevices; +} + +std::vector Overseer::getCaptureEndpoints() { + return captureDevices; +} + +void Overseer::updateEndpointInfo(std::wstring endpointId) { + log_wdebugcpp(L"new name Endpoint id: " + endpointId); + //todo: reintroduce capture devices + for(auto ep : playbackDevices) { + if (ep->getId() == endpointId) { + ep->updateName(); + osh->updateFrontEndpointName(ep); + break; + } + } +} + +Overseer::~Overseer(){ + log_debugcpp("cum"); + deviceEnumerator->Release(); + for(unsigned long long i = 0; i < playbackDevices.size(); i++){ + delete(playbackDevices.at(i)); + } +} + +void Environment::populateSystemValues() { + updateColors(); + Environment::startup = checkStartup(scope); +} + +void Environment::openControlPanel() { STARTUPINFOEXW startupConfig; PROCESS_INFORMATION processInfo; SecureZeroMemory(&startupConfig, sizeof(STARTUPINFOEXW)); @@ -774,7 +830,7 @@ void Overseer::openControlPanel() { } } -ProcessedNativeEvent Overseer::processTopLevelWindowMessage(void* msg) { +ProcessedNativeEvent Environment::processTopLevelWindowMessage(void* msg) { #ifdef WIN32 MSG *message = static_cast(msg); switch(message->message) { @@ -792,7 +848,7 @@ ProcessedNativeEvent Overseer::processTopLevelWindowMessage(void* msg) { #endif } -ProcessedNativeEvent Overseer::updateColors() { +ProcessedNativeEvent Environment::updateColors() { // DwmGetColorizationColor( WM_DWMCOLORIZATIONCOLORCHANGED DWORD value = 0; DWORD size = sizeof(DWORD); @@ -801,57 +857,119 @@ ProcessedNativeEvent Overseer::updateColors() { //Theme bg color result = RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size); - - this->lightMode = (bool)value; + lightMode = (bool)value; //Accent color result = RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\DWM", L"ColorizationColor", RRF_RT_REG_DWORD, nullptr, &value, &size); - if (result == ERROR_SUCCESS) { - this->accentColor = value; - } else this->accentColor = 0xffffffff; + accentColor = value; + } else accentColor = 0xffffffff; return ProcessedNativeEvent::COLORS; } -bool Overseer::isLightMode() { - return this->lightMode; +bool Environment::checkStartup(HKEY rootKeyFlags) { + //LSTATUS result; + DWORD typeReturned; + + //Checking if app entry exists + if(RegGetValueW(rootKeyFlags, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", LAPP_NAME, RRF_RT_REG_SZ, &typeReturned, nullptr, nullptr) != ERROR_SUCCESS && typeReturned != REG_SZ) + return false; + else return true; } -uint32_t Overseer::getAccentColor() { - return this->accentColor; -} +void Environment::updateStartupConfig(bool onStartup) { + DWORD typeReturned; + wchar_t regSubKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run\\"; -NGuid Overseer::getGuid() { - return guid; -} - -std::vector Overseer::getPlaybackEndpoints() { - return playbackDevices; -} + if(!onStartup) { + HKEY runKey; + if (RegOpenKeyExW( + scope, + regSubKey, + 0, + KEY_SET_VALUE, + &runKey + ) + == ERROR_SUCCESS) { + RegDeleteValueW(runKey, LAPP_NAME); + RegCloseKey(runKey); + } + } else { + LSTATUS result; + uint32_t cPathLen = 0; + wchar_t* cPath = getExeAbsPath(&cPathLen); + wchar_t* regPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS + 2, sizeof(wchar_t)); + //char* v = 0xFF'00'00'00'00'00'00'21; + regPath[0] = L'"'; + memcpy(regPath + 1, cPath, sizeof(wchar_t) * cPathLen); + memcpy(regPath + cPathLen + 1, L"\"", sizeof(wchar_t) * 2); -std::vector Overseer::getCaptureEndpoints() { - return captureDevices; -} - -void Overseer::updateEndpointInfo(std::wstring endpointId) { - log_wdebugcpp(L"new name Endpoint id: " + endpointId); - //todo: reintroduce capture devices - for(auto ep : playbackDevices) { - if (ep->getId() == endpointId) { - ep->updateName(); - osh->updateFrontEndpointName(ep); - break; - } + result = RegSetKeyValueW( + scope, + regSubKey, + LAPP_NAME, + REG_SZ, + (void*)regPath, + (cPathLen + 3) * sizeof(wchar_t)); + /* + * if (result != ERROR_SUCCESS) { + * wchar_t* error; + * FormatMessageW( + * FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + * nullptr, + * (DWORD)result, + * LANG_USER_DEFAULT, + * error, + * 1, + * nullptr); + * LocalFree(error); + * } + */ + free(cPath); } + return; } -Overseer::~Overseer(){ - log_debugcpp("cum"); - deviceEnumerator->Release(); - for(unsigned long long i = 0; i < playbackDevices.size(); i++){ - delete(playbackDevices.at(i)); +void Environment::setStartupConfig(bool onStartup) { + uint32_t cPathLen = 0; + wchar_t* cPath = getExeAbsPath(&cPathLen); + wchar_t startupParam[] = L"--change-startup"; + uint32_t startupParamLen = (sizeof(startupParam) / sizeof(wchar_t)) - 1; + wchar_t* completeParam = (wchar_t*)calloc(startupParamLen + 3, sizeof(wchar_t)); + memcpy(completeParam, startupParam, sizeof(wchar_t) * startupParamLen); + if (onStartup) + memcpy(completeParam + startupParamLen, L" 1", sizeof(wchar_t) * 3); + else + memcpy(completeParam + startupParamLen, L" 0", sizeof(wchar_t) * 3); + + if(scope == HKEY_LOCAL_MACHINE) { + ShellExecuteW( + NULL, + L"runas", + cPath, + completeParam, + NULL, // default dir + SW_SHOWNORMAL + ); + } else { + Environment::updateStartupConfig(onStartup); } + free(cPath); + free(completeParam); + return; +} + +bool Environment::isLightMode() { + return lightMode; +} + +bool Environment::isToRunAtStartup() { + return startup; +} + +uint32_t Environment::getAccentColor() { + return accentColor; } //int Overseer::getCaptureEndpoints(std::vector *captureEndpoints); diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 4e67a09..7c7c412 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -4,10 +4,13 @@ #include "backsessionclasses.h" #include "global.h" #include "contclasses.h" +//#include "environment.h" class EndpointVolumeCallback; class Session; -std::string getPath(SettingsTargetDirectory target, bool create); + +wchar_t* getExeAbsPath(uint32_t *exeAbsPathLength); +std::string getSettingsPath(SettingsTargetDirectory target, bool create); // Convert a wide UTF16LE string to an UTF8 string static inline std::string utf16ToUtf8(const wchar_t* wstr) { @@ -146,12 +149,6 @@ class Overseer { public: Overseer(); NGuid getGuid(); - void populateSystemValues(); - void openControlPanel(); - ProcessedNativeEvent processTopLevelWindowMessage(void* msg); - ProcessedNativeEvent updateColors(); - bool isLightMode(); - uint32_t getAccentColor(); std::vector getPlaybackEndpoints(); std::vector getCaptureEndpoints(); @@ -171,11 +168,11 @@ class Overseer { ~Overseer(); private: + std::wstring exeAbsPath; + void initCOMLibrary(); NGuid guid; - bool lightMode; - uint32_t accentColor; IMMDeviceEnumerator *deviceEnumerator; EndpointSituationCallback epsc; @@ -202,3 +199,22 @@ class EndpointNewSessionCallback : public IAudioSessionNotification { ULONG ref = 1; EndpointHandler *eph; }; + +namespace Environment { + void populateSystemValues(); + void openControlPanel(); + ProcessedNativeEvent processTopLevelWindowMessage(void* msg); + ProcessedNativeEvent updateColors(); + bool checkStartup(HKEY rootKeyFlags); + void updateStartupConfig(bool onStartup); + void setStartupConfig(bool onStartup); + bool isLightMode(); + bool isToRunAtStartup(); + uint32_t getAccentColor(); + + static std::wstring exeAbsPath; + static bool lightMode; + static bool startup = false; + static HKEY scope; + static uint32_t accentColor; +}; diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 067a32f..368f466 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -3,7 +3,7 @@ void setConfigDirToDefaults() { #define tryFileDir(dir, create) do { \ - OverseerHandler::settingsPath = getPath(dir, create); \ + OverseerHandler::settingsPath = getSettingsPath(dir, create); \ set = ini::UserSettings::createSettings(OverseerHandler::settingsPath.c_str()); \ if(set) { \ return; \ @@ -247,24 +247,36 @@ std::string OverseerHandler::getSettingsPath(){ return OverseerHandler::settingsPath; } +void OverseerHandler::updateStartupConfig(bool onStartup) { + Environment::updateStartupConfig(onStartup); +} + +void OverseerHandler::setStartupConfig(bool onStartup) { + Environment::setStartupConfig(onStartup); +} + void OverseerHandler::populateSystemValues() { - this->os->populateSystemValues(); + Environment::populateSystemValues(); } void OverseerHandler::openControlPanel() { - this->os->openControlPanel(); + Environment::openControlPanel(); } ProcessedNativeEvent OverseerHandler::processTopLevelWindowMessage(void* msg) { - return this->os->processTopLevelWindowMessage(msg); + return Environment::processTopLevelWindowMessage(msg); } bool OverseerHandler::isLightMode() { - return this->os->isLightMode(); + return Environment::isLightMode(); +} + +bool OverseerHandler::isToRunAtStartup() { + return Environment::isToRunAtStartup(); } uint32_t OverseerHandler::getAccentColor() { - return this->os->getAccentColor(); + return Environment::getAccentColor(); } std::vector OverseerHandler::getPlaybackEndpoints() { diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index 2a4a9f0..ac4ecdc 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -105,10 +105,13 @@ public: static void setSettingsPath(std::string path); static std::string getSettingsPath(); static inline std::string settingsPath; + void updateStartupConfig(bool onStartup); + void setStartupConfig(bool onStartup); void populateSystemValues(); void openControlPanel(); ProcessedNativeEvent processTopLevelWindowMessage(void* msg); bool isLightMode(); + bool isToRunAtStartup(); uint32_t getAccentColor(); //void setChangeFrontDefaultsFunction(std::function changeFrontDefaults); diff --git a/src/debug.h b/src/debug.h index 176825a..d4b9195 100644 --- a/src/debug.h +++ b/src/debug.h @@ -2,6 +2,8 @@ #if defined (QT_DEBUG) || defined (DEBUG) || defined (_DEBUG) +#define PIPE_NAME "Mixerq-dev" + #ifdef INIT_FILELOG std::wstring_convert, wchar_t> converter; FILE* fileLog; @@ -72,7 +74,8 @@ std::bitset varToBitset(T info) { #define print_as_binary(info) #define log_to_file(fmt, cnt...) #define initialize_file_log() false -#define close_file_log_buffer() +#define close_file_log_buffer() +#define PIPE_NAME "Mixerq" #endif /* Here as a quick reference, in case smthn similar is needed again */ diff --git a/src/global.h b/src/global.h index 7c02902..ba5678b 100644 --- a/src/global.h +++ b/src/global.h @@ -16,6 +16,9 @@ //#include "settings.h" //TODO: Use tr();? QTranslator +#define APP_NAME "MixerQ" +#define LAPP_NAME L"MixerQ" + #define STRING_MUTE "Mute" #define STRING_UNMUTE "Unmute" #define STRING_QUIT "Quit" @@ -38,7 +41,6 @@ #define LSTRING_UNNAMED_SESSION L"Unnamed session" - //INIT BACK enum SettingsTargetDirectory { diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 15f3768..fa91703 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -37,7 +37,6 @@ CustomWidgetEvent::CustomWidgetEvent(QEvent::Type type, T payload) : QEvent(t this->payload = payload; } - /* * MeterSlider::MeterSlider(Qt::Orientation orientation, QWidget* parent) { * //style = new MixerStyle(); @@ -225,7 +224,7 @@ QRect MainWindow::setSizePosition(QScreen* screen, int width, int height) { QRect availableRes = screen->availableGeometry(); int arx1, arx2, ary1, ary2; availableRes.getCoords(&arx1, &ary1, &arx2, &ary2); - log_debugcpp("Available res: " + std::to_string(arx1) + " " + std::to_string(arx2)+" " + std::to_string(ary1) + " " + std::to_string(ary2);) + log_debugcpp("Available res: " + std::to_string(arx1) + " " + std::to_string(arx2)+" " + std::to_string(ary1) + " " + std::to_string(ary2)); if(height > (ary2 - ary1)) height = (ary2 - ary1); log_debugcpp("Height res: " + std::to_string(height)); @@ -959,11 +958,15 @@ HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { } }); - text = "&" STRING_STARTUP; startup = new QCheckBox(text, this); - //connect(openCP, &QPushButton::clicked, [](){ osh->openControlPanel(); }); + if(osh->isToRunAtStartup()) { + startup->setChecked(true); + } + connect(startup, &QCheckBox::stateChanged, [this](){ + osh->setStartupConfig(startup->isChecked()); + }); widgetLayout->addWidget(openCP , 0, 0, 2, 2); widgetLayout->addWidget(channels, 0, 2, 1, 2); diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index 8b34385..b036c72 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -7,13 +7,16 @@ OverseerHandler *osh = nullptr; ini::UserSettings *set = nullptr; - -QApplication* createApplication(int &argc, char *argv[]) -{ + +bool startupRun = false; +bool onStartup = false; +char* userSettingsPath = nullptr; + +QApplication* createApplication(int &argc, char *argv[]) { return new QApplication(argc, argv); } -bool isSingleInstanceRunning(QString appName) { +bool isInstanceRunning(QString appName) { QLocalSocket socket; socket.connectToServer(appName); bool isOpen = socket.isOpen(); @@ -21,7 +24,7 @@ bool isSingleInstanceRunning(QString appName) { return isOpen; } -QLocalServer* startSingleInstanceServer(QString appName) { +QLocalServer* startInstanceServer(QString appName) { QLocalServer* server = new QLocalServer; server->setSocketOptions(QLocalServer::WorldAccessOption); server->listen(appName); @@ -32,12 +35,34 @@ void closeDebugFileLog() { close_file_log_buffer(); } -char* parseCmdArgs(int argc, char* argv[]) { - if(argc == 1) return nullptr; - char arg[] = "--config-path="; - if(strstr(argv[1], arg)) { - return argv[1] + (sizeof(arg) / sizeof(arg[0])) - 1; - } else return nullptr; +void parseCmdArgs(int argc, char* argv[]) { + if(argc == 1) return; + //char* configPath = nullptr; + char* arg[] = { (char*)"--config-path=", (char*)"--change-startup" }; + + for (int i = 1; i < argc; i++) { + if(strstr(argv[i], arg[0])) { + userSettingsPath = argv[i] + strlen(arg[0]); + } + + if(strstr(argv[i], arg[1])) { + if (++i >= argc) return; + switch (argv[i][0]) { + case '0': + startupRun = true; + onStartup = false; + break; + case '1': + startupRun = true; + onStartup = true; + break; + default: + break; + } + return; + } + } + return; } /* set_terminate @@ -48,41 +73,49 @@ char* parseCmdArgs(int argc, char* argv[]) { */ int main (int argc, char* argv[]) { - /* - * QStringList styles = QStyleFactory::keys(); - * for(QString a : styles) { - * log_debugcpp(a.toStdString()); - * } + * Debug: logging report file */ - char* userSettingsPath = parseCmdArgs(argc, argv); - if (userSettingsPath) - set = ini::UserSettings::createSettings(userSettingsPath, true); - - if (set) - OverseerHandler::settingsPath = std::string(userSettingsPath); - else setConfigDirToDefaults(); - initialize_file_log(); atexit(closeDebugFileLog); - //Check if running - //https://stackoverflow.com/questions/48060989/qt-show-application-if-currently-running - //std::set_terminate(closeDebugFileLog2); - if (!isSingleInstanceRunning("Mixer")) - startSingleInstanceServer("Mixer"); - else exit(0); - - QApplication::setStyle(new MixerStyle(QStyleFactory::create("Fusion"))); //QApplication::setFont(font); //QPalette palette = QGuiApplication::palette(); - //todo: ez full apply os accent colorw + //palette.setColor(QPalette::Active, QPalette::Highlight, QColor(255, 192, 203, 200)); //QColor(30,30,30,100)); //QGuiApplication::setPalette(palette); osh = new OverseerHandler(); osh->populateSystemValues(); + + /* + * Arg parsing: (admin?) startup change run + */ + parseCmdArgs(argc, argv); + if (startupRun) { + if (!isInstanceRunning(PIPE_NAME)) exit(-1); + //if (startupChangeInfo->permissionChangeScope == NONE) exit(-1); + osh->updateStartupConfig(onStartup); + exit(0); + } + + //Check if running + //https://stackoverflow.com/questions/48060989/qt-show-application-if-currently-running + if (!isInstanceRunning(PIPE_NAME)) + startInstanceServer(PIPE_NAME); + else exit(0); + + /* + * Config file init + */ + if (userSettingsPath) + set = ini::UserSettings::createSettings(userSettingsPath, true); + + if (set) + OverseerHandler::settingsPath = std::string(userSettingsPath); + else setConfigDirToDefaults(); + StylingHelper::setBackgroundColor(osh->isLightMode()); //qRegisterMetaType(); From c87c8d099061d1d9353692220245e9b6cbff7573 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 7 Jan 2025 18:53:17 +0100 Subject: [PATCH 26/38] set: fix wrong save path / fixed null deref / more env refactor --- src/back/backlasses.cpp | 190 +++++++++++++++++++-------------------- src/back/backlasses.h | 5 +- src/cont/contclasses.cpp | 4 +- src/qt/qtclasses.cpp | 9 +- src/settings.cpp | 28 +++--- 5 files changed, 117 insertions(+), 119 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index e0238c7..a0f0e06 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -3,101 +3,6 @@ using namespace Environment; -wchar_t* getExeAbsPath(uint32_t *exeAbsPathLength) { - wchar_t *exeAbsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); - *exeAbsPathLength = GetModuleFileNameW( - NULL, - exeAbsPath, - UNICODE_STRING_MAX_CHARS - ); - return exeAbsPath; -} - -std::string getSettingsPath(SettingsTargetDirectory target, bool create) { - wchar_t* settingsPath = nullptr; - wchar_t settingsFile[] = L"\\settings.ini"; - uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; - wchar_t maxPathBypass[] = L"\\\\?\\"; - uint32_t exePathLength = 0; - wchar_t folderPath[] = L"\\mixerq"; - uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; - uint32_t folderPathLen = (sizeof(folderPath) / sizeof(wchar_t)) - 1; - wchar_t* roamingPath = nullptr; - - log_wdebugcpp(L"Bypass size: " + std::to_wstring((sizeof(maxPathBypass)/sizeof(maxPathBypass[0])))); - - switch(target) { - case HOME_DIR: - { - if(SHGetKnownFolderPath( - FOLDERID_RoamingAppData, - 0, - NULL, - &roamingPath) - == S_OK) { - //Retrieve path len - uint32_t pathLen = 0; - wchar_t currentChar = roamingPath[pathLen]; - while(currentChar != '\0') { - pathLen++; - currentChar = roamingPath[pathLen]; - } - - settingsPath = (wchar_t*)calloc(pathLen + - maxPathBypassLen + - folderPathLen + - settingsFileLen, - sizeof(wchar_t)); - memcpy(settingsPath, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); - memcpy(settingsPath + (maxPathBypassLen), roamingPath, sizeof(wchar_t) * pathLen); - CoTaskMemFree(roamingPath); - memcpy(settingsPath + (maxPathBypassLen + pathLen), - folderPath, sizeof(wchar_t) * folderPathLen); - log_wdebugcpp(L"Settings folder path: " + std::wstring(settingsPath)); - - if(CreateDirectoryW(settingsPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) { - memcpy(settingsPath + (maxPathBypassLen + pathLen + folderPathLen), - settingsFile, sizeof(wchar_t) * settingsFileLen); - std::string utf8path = utf16ToUtf8(settingsPath); - free(settingsPath); - return utf8path; - } - } - } - return nullptr; - break; - case APP_PATH: - { - //Executable dir - settingsPath = getExeAbsPath(&exePathLength); - - //reverse wcsstr - while(exePathLength >= 0) { - if(settingsPath[exePathLength] == '\\') { - memset(settingsPath + exePathLength, - 0, - (UNICODE_STRING_MAX_CHARS - exePathLength) * sizeof(wchar_t)); - break; - } else exePathLength--; - } - log_wdebugcpp(L"Exe folder: " + std::wstring(settingsPath)); - if((UNICODE_STRING_MAX_CHARS - exePathLength) > (settingsFileLen + 1)) { - memcpy(settingsPath + exePathLength, settingsFile, sizeof(wchar_t) * settingsFileLen); - std::string utf8path = utf16ToUtf8(settingsPath); - free(settingsPath); - return utf8path; - } - } - return nullptr; - break; - default: - return nullptr; - break; - } - return nullptr; - -} - EndpointNewSessionCallback::EndpointNewSessionCallback(EndpointHandler* eph){ this->eph = eph; } @@ -799,6 +704,100 @@ Overseer::~Overseer(){ } } +wchar_t* Environment::getExeAbsPath(uint32_t *exeAbsPathLength) { + wchar_t *exeAbsPath = (wchar_t*)calloc(UNICODE_STRING_MAX_CHARS, sizeof(wchar_t)); + *exeAbsPathLength = GetModuleFileNameW( + NULL, + exeAbsPath, + UNICODE_STRING_MAX_CHARS + ); + return exeAbsPath; +} + +std::string Environment::createSettingsPath(SettingsTargetDirectory target, bool create) { + wchar_t* settingsPath = nullptr; + wchar_t settingsFile[] = L"\\settings.ini"; + uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; + wchar_t maxPathBypass[] = L"\\\\?\\"; + uint32_t exePathLength = 0; + wchar_t folderPath[] = L"\\" LAPP_NAME; + uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; + uint32_t folderPathLen = (sizeof(folderPath) / sizeof(wchar_t)) - 1; + wchar_t* roamingPath = nullptr; + + log_wdebugcpp(L"Bypass size: " + std::to_wstring((sizeof(maxPathBypass)/sizeof(maxPathBypass[0])))); + + switch(target) { + case HOME_DIR: + { + if(SHGetKnownFolderPath( + FOLDERID_RoamingAppData, + 0, + NULL, + &roamingPath) + == S_OK) { + //Retrieve path len + uint32_t pathLen = 0; + wchar_t currentChar = roamingPath[pathLen]; + while(currentChar != '\0') { + pathLen++; + currentChar = roamingPath[pathLen]; + } + + settingsPath = (wchar_t*)calloc(pathLen + + maxPathBypassLen + + folderPathLen + + settingsFileLen, + sizeof(wchar_t)); + memcpy(settingsPath, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); + memcpy(settingsPath + (maxPathBypassLen), roamingPath, sizeof(wchar_t) * pathLen); + CoTaskMemFree(roamingPath); + memcpy(settingsPath + (maxPathBypassLen + pathLen), + folderPath, sizeof(wchar_t) * folderPathLen); + log_wdebugcpp(L"Settings folder path: " + std::wstring(settingsPath)); + + if(CreateDirectoryW(settingsPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) { + memcpy(settingsPath + (maxPathBypassLen + pathLen + folderPathLen), + settingsFile, sizeof(wchar_t) * settingsFileLen); + std::string utf8path = utf16ToUtf8(settingsPath); + free(settingsPath); + return utf8path; + } + } + } + return nullptr; + break; + case APP_PATH: + { + //Executable dir + settingsPath = getExeAbsPath(&exePathLength); + + //reverse wcsstr + while(exePathLength >= 0) { + if(settingsPath[exePathLength] == '\\') { + memset(settingsPath + exePathLength, + 0, + (UNICODE_STRING_MAX_CHARS - exePathLength) * sizeof(wchar_t)); + break; + } else exePathLength--; + } + log_wdebugcpp(L"Exe folder: " + std::wstring(settingsPath)); + if((UNICODE_STRING_MAX_CHARS - exePathLength) > (settingsFileLen + 1)) { + memcpy(settingsPath + exePathLength, settingsFile, sizeof(wchar_t) * settingsFileLen); + std::string utf8path = utf16ToUtf8(settingsPath); + free(settingsPath); + return utf8path; + } + } + return nullptr; + break; + default: + return nullptr; + break; + } + return nullptr; +} + void Environment::populateSystemValues() { updateColors(); Environment::startup = checkStartup(scope); @@ -932,6 +931,7 @@ void Environment::updateStartupConfig(bool onStartup) { } void Environment::setStartupConfig(bool onStartup) { + //TODO: Use the cache!!!! lol uint32_t cPathLen = 0; wchar_t* cPath = getExeAbsPath(&cPathLen); wchar_t startupParam[] = L"--change-startup"; diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 7c7c412..071e0e1 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -9,9 +9,6 @@ class EndpointVolumeCallback; class Session; -wchar_t* getExeAbsPath(uint32_t *exeAbsPathLength); -std::string getSettingsPath(SettingsTargetDirectory target, bool create); - // Convert a wide UTF16LE string to an UTF8 string static inline std::string utf16ToUtf8(const wchar_t* wstr) { if(!wstr || wstr[0] == '\0') return std::string(); @@ -201,6 +198,8 @@ class EndpointNewSessionCallback : public IAudioSessionNotification { }; namespace Environment { + wchar_t* getExeAbsPath(uint32_t *exeAbsPathLength); + std::string createSettingsPath(SettingsTargetDirectory target, bool create); void populateSystemValues(); void openControlPanel(); ProcessedNativeEvent processTopLevelWindowMessage(void* msg); diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 368f466..e9561f0 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -3,8 +3,8 @@ void setConfigDirToDefaults() { #define tryFileDir(dir, create) do { \ - OverseerHandler::settingsPath = getSettingsPath(dir, create); \ - set = ini::UserSettings::createSettings(OverseerHandler::settingsPath.c_str()); \ + OverseerHandler::settingsPath = Environment::createSettingsPath(dir, create); \ + set = ini::UserSettings::createSettings(OverseerHandler::settingsPath.c_str(), create); \ if(set) { \ return; \ } else OverseerHandler::settingsPath.clear(); \ diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index fa91703..376cdcd 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -947,9 +947,12 @@ HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { channels->setChecked(true); } connect(channels, &QCheckBox::stateChanged, [this, parent](){ - set->setValue("show_channels", channels->isChecked(), sizeof("show_channels")); - if(!OverseerHandler::settingsPath.empty()){ - set->save(OverseerHandler::settingsPath.c_str()); + //TODO: Find a better way to auto no-op when there's no settings file + if(set) { + set->setValue("show_channels", channels->isChecked(), sizeof("show_channels")); + if(!OverseerHandler::settingsPath.empty()){ + set->save(OverseerHandler::settingsPath.c_str()); + } } if(parent) { QEvent explosion = QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow); diff --git a/src/settings.cpp b/src/settings.cpp index b45a9f1..6845254 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -27,7 +27,7 @@ namespace ini { isCRLF = true; *(nextLine - 1) = '\0'; } else if (nextLine) *nextLine = '\0'; // temporarily terminate the current line - log_debugcpp("curLine: " + std::string(curLine) + " "); + log_debugcpp("[SET] curLine: " + std::string(curLine) + " "); separator = strchr(curLine, '='); if(!separator) @@ -36,7 +36,7 @@ namespace ini { key = trimAndAllocate(curLine); value = trimAndAllocate(separator + 1); values.try_emplace(key, value); - log_debugcpp("ini Map size: " + std::to_string(values.size())); + log_debugcpp("[SET] ini Map size: " + std::to_string(values.size())); *separator = '='; nextIteration: @@ -76,10 +76,10 @@ namespace ini { void UserSettings::setValue(char* key, bool value, uint64_t keySize) { char *newKey; - log_debugcpp("Pos value: " + std::to_string((intptr_t)pos)); - log_debugcpp("Neg value: " + std::to_string((intptr_t)neg)); + log_debugcpp("[SET] Pos value: " + std::to_string((intptr_t)pos)); + log_debugcpp("[SET] Neg value: " + std::to_string((intptr_t)neg)); if (auto search = values.find(key); search != values.end()) { - log_debugcpp("Previous value: " + std::to_string((intptr_t)values[key])); + log_debugcpp("[SET] Previous value: " + std::to_string((intptr_t)values[key])); if (!(search->second == pos || search->second == neg)) { free(search->second); } @@ -90,22 +90,16 @@ namespace ini { } newKey = (char*)calloc(keySize, sizeof(char)); + memcpy(newKey, key, keySize * sizeof(char)); values.insert(std::make_pair(newKey, value ? pos : neg)); return; } bool UserSettings::save(const char* path) { - wchar_t maxPathBypass[] = L"\\\\?\\"; - uint32_t maxPathBypassLen = (sizeof(maxPathBypass)/ sizeof(wchar_t)) - 1; - if(!path) return false; uint64_t convertedPathSize = 0; - wchar_t* convertedPath = Utf8toUtf16(path, &convertedPathSize); - if(!convertedPath) return false; - wchar_t* utf16Path = (wchar_t*)calloc(maxPathBypassLen + convertedPathSize, sizeof(wchar_t)); - memcpy(utf16Path, maxPathBypass, sizeof(wchar_t) * maxPathBypassLen); - memcpy(utf16Path + maxPathBypassLen, convertedPath, sizeof(wchar_t) * convertedPathSize); - free(convertedPath); + wchar_t* utf16Path = Utf8toUtf16(path, &convertedPathSize); + if(!utf16Path) return false; #define releaseBeforeReturn() do { \ CloseHandle(settingsHandle); \ @@ -147,7 +141,8 @@ namespace ini { 0, CREATE_ALWAYS, NULL); - if(settingsHandle == INVALID_HANDLE_VALUE) { + if(settingsHandle == INVALID_HANDLE_VALUE) { + log_debugcpp("[SET] Can't save to file: " + std::to_string(GetLastError())); releaseBeforeReturn(); return false; } @@ -197,7 +192,8 @@ namespace ini { 0, (create ? OPEN_ALWAYS : OPEN_EXISTING), NULL); - if(settingsHandle == INVALID_HANDLE_VALUE) { + if(settingsHandle == INVALID_HANDLE_VALUE) { + log_debugcpp("[SET] Can't create settings file: " + std::to_string(GetLastError())); releaseBeforeReturn(); return nullptr; } From cdde1ce9b8b8e9ec1a344bac84b373dac4a4caff Mon Sep 17 00:00:00 2001 From: Hane Date: Sun, 12 Jan 2025 23:16:36 +0100 Subject: [PATCH 27/38] installer done, recipe cleanup --- assets/installer.ico | Bin 0 -> 196630 bytes assets/installer.xcf | Bin 0 -> 103610 bytes assets/uninstaller.ico | Bin 0 -> 196273 bytes bueno.bat | 5 +- install/installer.nsi | 233 +++++++++++++++++++++++++---------------- install/version.nsi | 6 +- qtest.pro | 30 ++++-- 7 files changed, 172 insertions(+), 102 deletions(-) create mode 100644 assets/installer.ico create mode 100644 assets/installer.xcf create mode 100644 assets/uninstaller.ico diff --git a/assets/installer.ico b/assets/installer.ico new file mode 100644 index 0000000000000000000000000000000000000000..b4e6f82b645cfabb8830e8421e97a78897465330 GIT binary patch literal 196630 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

R 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 literal 0 HcmV?d00001 diff --git a/assets/installer.xcf b/assets/installer.xcf new file mode 100644 index 0000000000000000000000000000000000000000..96669f1efd409adaa5b5e92494b1dbc41ee422a4 GIT binary patch literal 103610 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

ik;N8#zJ;L|4<*un-#IspWrbcsn(_ijw`|D%G1P zXMoR_QrHm;0jYJEDdd@te{6VFsX_-27P^Yi(^j*sIXz$?vT(43rn64QQs5t>oVn~M zG+m^zhO0DhOey8u9Api22MvZFfL-XU*3W!Aa zfOgGiw-J?7QI6OeM&ACh;a(Jaq#U7R&q&)!z4hsKR2s2Nh?L%#2IyP0^kM`t;nvb$ zg?x)bSdl}P##*Uf+9qTi6(K6DCAw$QG7x0jph31COnVrcp;R|*R2gFA4@9_j*0MnV zMiOYxH4+{Uhh2Kh>S#?CB~MdOeMV0DT=&Tqs2i-m5Q33NTLST-5M_`p>QmG=xtVUd zFB3v(s**;h)3{Lckb&$!qGKkl^M*4e+A{_X8a!yQqvwguJQ@CgV%Nwh+R&sQRH}ai zS{8;SCZ5s_^YfTBXLaXFcS-G5s(z6N{ zKXc}^tgFxGU%hd=sG_d*@6gqkTu{eNy)Jc*wI4lp`qF(=bL|f=9nZ~Wtr|F{j+#f@ z+#5AN#jbbS)&%>Acc%0whSz%4M%nHhb&j|>r>eLu?y6ktn9a47?papmOYNGt0|ukY1LO%p4~>#DeT3J?u5) zT1RbooNO!0zW_^-qF`BN3yr1XUHa!dB-Sz6E{iypQHQ;3ee*zNl?0Q0ShBhojv!@- zWm`5%e)DjcMN);#4I#5`M&}Q7?I|fw7cK^jDxITFBz&8|qN}4ZFu@2gKnLGaFzoO7O zw^;J&c4$L`cL4Rgm($L4uCeKX0ClSaA=cUlTM)mU7Md_R5XEfJ$W+c{eaY$(3y68Z zf>upEd1C#ru`LLp6v8bbsDrwA8=6e_2bQMUFsXh1U1l*6ux_OoNfcRasNUXNkr~_? zYHqB<&zf~lOHJ*g$90X(ZDFQwsHv9KN>!jT{IIs64gFy2Ba3SX?W(E_S5!X2#oui~ z3k!=|QCVqMcvVfzOgy8lD!Wou_?1qDTVB<|?5$TY1uHC&P2p&)qLdh}+_I_=9o0|; zzg(MTUPT+?*F};Qm+f5E*p*g=U(Rx++=djQw?b7GHK=kEnpI|&`topZRPd>C;9Q}p zQlH81RU=o7zVyo4$k?KhQe%;7%auH4TI9;&$}6cruIh@4Sbb@hNV3~h5lvPl zel^$O$|u)eDKR+SqY9_oE7Rp!C4Qq+hkP+*R;gX0B}BSlnIl&+a*rZcR*73&!kk8a zYV%7)IJ?9wmIB=N3TSR=Sy`aeDpAEgI(@IIT$ROgX%*{2YpsqegvK+hEQah>u~+I+ zW@`tlTU3mQu3HvmRz*I2>s26}Dp8bX7n?=0erJiym9HrxBx_{B=|c!iH56Dyn$7ZXsnsL$<0C&^@y=EcQWf%b?Z*hy(;UBw#Z zXNJ`|9G8%sNw_5HaUmuA)DYEiQbs4gEwaE$_=#a9ut4d|B8gR);#$cyF&r%ao?H?M zgRWgA{B0yvE@dV4?WJapUb31L4L$k>DXE(u&PYnotZrg*=95cP3&Qb+8OZfxbm(4E zGEr2lq|lKkB_<`K3=gx|A}xgQNhu_<42&cuNg@&@k_by8%T?j!vRy89GR%@Jml-6> z8Z*kB9L-Ewni-iSvo1QqoSc}Lq?xG~_wN)XS3;DEWZlXlh8(&eeT5X%Dg}LeDYbHC z{$B-|k%FXHx-3Z++giN;?+a<>lN6DY*})1{^64%~WLhYn`GZ*^8yiK#NIp({!AC|v zwiwqwm(SV#`u&>`E+6o@d}X1D|RrwXT? z(DKM-=w$S}DwSk%KUzEFbLwcSN;lRTt+lsKWMv*adq2r&FFU?y*he2ZAG@hr%l^9& zuFaj$H|Ar%ulm^Qf8u8t-Pe3Bit(6_+fj1&{UnzMLc#ox)kg~Lxc#x$_hY-S`^mtv z9~oZpV&|ja$Kk#o_wDO^;%B~s?R(27!H=wu1AW^*`4naOrx{pF&J4CbGSRAAA3Ny8 ztxx?gzR!DqUaXYgx9=yC6@9t&>C3VXLE+x_<6z(5C*G$6>`(p11S4FxqVLCDQLaOD z=RO>d4*iP++*L+B-8bfwwqm1rz-OP0dJ!-9FS_=%P`j&7%umBq%{jf{v!8u7{{_R_ z_KDLs?i1%ze+MC8n?67I9eQQS)1uD*B>1WK*;V!x3vXiWdo<;_PXeF1pM6;hka0~$ zR|K=kW`E|VLa6$x8|;D4P;gH|fJfuwJ`H{r{M_reg4!FVSf3GL#-HLZ@R|4dXV&L| zet!0gcq~2hS0d(H#;;!J#|x01>?$Z%IGSYli~aly0V8~1VE-#fUOFJYADZYdmjw;? z$iO_d@Y`ZV;V(shfj?4zZ@``)E<8t{!v}r%7hilC`$f!`f&T7*b>dZ?{T%(-FCc2! zFFJkcu$KuBOv|dFkte=GQo#syN%YJOKT*6J?Y2Z?$;mj2t(0(dJ!y@JQIZXY%>74~<4!oo-Q44MUtuJC|uW^e&%=}xAoW~d2k}P%keI`{#oX6;jv>Em^wEc7S+#45d_+}*N)w0YP4N?o&~gc1A7-LnF-D3 zo~Ak{*OPU!m-~o`+j#bcT=Q6%t~kL}!|CtlqI5p$pQhTTGv9&DIqF}0(Wq}ddn{M} zMLwt{H|ALIsCVQpb2u;lSPbiAzYsBQJ%&Dq2vcZOL&K zvwktMwVr&IqBriSlVsYowKmt5!dr2?s z!B%gf42^$fwM!jESJV-op@OXedng}Ko=Nj~F}QW#@=3o`iXoRE~s zSA1eJUJoIh22%J|N#6hOmn>>DudJ!01XgO)VkXF3Vda^eWOPeHZI~FxDwKyAi?+@w zRNW*yv@C_6i!7>Hm}LThpF;Hs zUSh`cA|tgYiZxn`7P5?4=^RvvK;l}y?(6h}||Jf}{ZK3&Z)XZo{ZXNG3XoIZV;ZL};OI7o60 zBUiKbDAf+7__u9|0(*_>_8Y9RBx0g=o@hpE&Dk~!)<`>nKeO-E)m*P5qv&E~8t z#>Fq6{+^Af1WefM`&}_ou+a9QP}>q%yiOU$tD=gavu4NW!8B(!Jjo_wqEaiL zbd6LQ>6^$j<(!RK_H2KS!z3_gUY|imn#jzH${HO^-7{1enR?QxOjc`Fn2w!oGwsY7 zVacfm)<4Oq=YdK$$JT4lX~~KkG+x#-?3n?kxizCcQkm2i5wrZ;+B&*slH5_#r#mxZ zX4*5%={SVYA)+A;ZgzWmkUY)PrcRyGPqNXV`;i6>8y;~5*SC@kTqIv4qjn#kW@!iVnkkbbStB(L zmU}{_KK{04+{1LFn&@iZ^R82Q%uF`>m4ET2jI{1}I)m99xsOJ3(4hI1IX*1IrnF1E zHN~GA6LD~7hg64@x^21-<*C#z(@XNT$#kYNMMgC#FqtDzCVM>BCRMYKigZe3p!H-{ zX^$frqyG9#6otFKJ;@}8np}Z53%L+e-*WP3k|%{!SW7;JKu=3*lo@7GWv5BzM00}s zc|Z1Kgmj2G0+*U0p`p?^iDhURsXhJ)4^R@I@U>4iZ{ma)a;c2g&->v$rH4QbYKhh+ z`RXNLAyONm4dc{!`)e~JIKj>ceJ#zxajAb@wC^J#N+zDUkxXNWnJ~!|<|#VC$sq6e zHpxc26q_)5QmhBnk=t9M9iB98too{b9M-LAlY(D6<7>y=xpeV-?tv{!hjmOYHKaX~ z%^qQn#8qpwN^hoZ<0j9THDkiKAki1*S20Fw@miUT_>m21jXp2a)QHC;wSY8h+*BS_ zGro=;r@m5S<=DXL3pk6(B$WP+PK*)G$d=LLrp}!=Z|;n7YAh3EtpAlW)*92hb z4Pqy9VHd-Q#GZu5Or1Ml&GV*>wa0|U@&v(8%Vo$EhwHT7i*}e~YLq&3SV;Pmx%PZ_ z8nGzpA>6daj0q6Cg6*%4+d@oKVtX-%w`t~#Io1MmzB_$vdSG-2AFcFsd$gNAdQ=y~ z9fFe{z3L6oCL}Y}9W!ITv%s4^Hii%tNBU_LM&%}JV0)yM_^b`*cAGd?%~uQD`7_1@ zq&G)eqs)=ssL?jeE%zhx4v828(`Q)otoh!|bc^tq5kX>agp&-&dR7|ePrqO=fV~ z^tp4LdEq(ZN2GR6^GDce;SuzhLP}gm&&U9XJ&^$zHDiv&>&}}s))IfbG&|KIcBqYD zAs#1$`v8+V8ygf2C-^9RCa;;f?epf&n~^>ukQS>)&Zc}d5|jLGU1rNr1WlhgS6-v; zv~=77F({@E52o7OP-a?4Pgo*r7eox2;bCKD&35L7=1onfLugS920zK;kFaq4!{mDt zcSI4jec0%k%se%>ZQk4|c-fKaj|ilOMNV5fYqZG#;4v^yjBe(z(5M;uo$5{jK{AKPK2@<DVh5uU*Ps^*&uT4zn4Hg(FRiC+cBw2U49HIL}o zOO~j`YLUBe!4+oO?GdT;jXBaCGh6ewESxuU23aO&1jn|p0?hb&!n~zR)e@5|-oh;W zi|QxqN0l1&XzINA^B2rtuyE1b*|S)BP8MMZ>+;826TQg`)e3WYc-c~^&EFAglx@)u zIPTW*bIG${!J;J#=FO#Br%!NrsQNtiy{Sv=m1;%%a&j%+9D@#J)JO?Hs~IzPhE%g? z@v_CzqjP3YA*!oU)@;1wr%qq8YE^J$XvOkn%QnRrB~v?I4-<5rrOTHsUc6}G{CTsc z5P!uBrhVG<8PjJjbyl0JTDKg&n3s3)!lj_meowzs)K@j*W-eT~Xz|kJBE^zL3m42F zn2G=_7Q3^&*~^?Y?xy^@IHT^mM4Cxh+C)Zd@iMidlKPQ*-prXZX2?2l&YZb(m#tm5 zPObIMv_4}rY>vLJW-MB~WVuB<-DOJ`E0$i~9D3ecw$57bZNC4k@wj-oyhvG7&WCZ; zs@2x2ww245FPb+`9;x#esD<9r^=gB=e)Gd;jrygsg8J!_bu>uYvuF3WnKo~hJmv_6 zve%mH%(d1!Z~X?cUdf4MMM6Z3c15&4>4=W3YMr-sU4X3C`r8}8MLZJCM=+J-qc!j4*`*G01b(ixE~_PU_X(thuVWD$dPd89Rw`S7IEzwHEjokNz; z`gQ9!BoOPOvIvXb)WQ|THl$zHaFaf)YH+~-)8x?q9=Lm_4c{|dD=EcGO4Vh=uCPt z9~h&*n1B|8)J5MGO-GJO*GYFHSw%D!EfO&HKIm zvMIcoFL48jG^PpJBBDvyp{esUbUYfE+gr9+)Z}g6IW=pVRd^?8G2bGM7+?)39ko49LdwX!3Nd@Lswaq+=@3rn)Tx$<09U&Rg z4;)*IV^X-a&E2|ri?h|)CjDcg5=WPfGYZ06I_?tJY8}7n>p02KwoPl+Y>wI1PGIHs zqYRzsz!e&DrW8jM6Y+^Bg^uM^yJ`_<+nNQlR}zECfB9!^uisHsT2g%P=9P;_WGae> z3F>nDKseHB9VxnH@ucyS*Y4O6*zWA`cd}5c&6gVn=$bM?VmK|MoTDv;qASN|O%5xY|b6?}0O%p!( z=;OYhPT0I-r_E3W@Q7ylQibNf9rqFj#@37t+opUB+`u3I{Pm{oyLQLya&<&2i|J;Z z)+y4=bPtLP3$duX=YOJpZ+;L?8M|&rx83%hE{3#1d`=`b*4@8uuzIKY-S^)A{fM-c@@|INgVbVZ|k$wwedL zbzcU0g?kU#L`@QN8?)Df$A2Jo+MEh~@W8&=?+1E?dXL^lAnjg%PjD}Dfw^N#QD<;r z*Iqc`gS*muQ_#CpckGT-WbO^pi5vyG+uuX~HsnNV#e+U`%Yb07&>!dT*o{lPxi^N9 zx8x5t|DGdLF4J50aj;kWr^~nF4bGh28!O|%pSj21&Doq1V60t?+{{%e!CuW@uG+F) z9LIO>vG>W0(6cF0CJXxt5mD-0@rOXKrrtwWZQ7!p*Z0KkbM~6LX1Gpw48!sd0=*i0 z4_~!@6QbsIKq*}7w|y-y}G zx8R@`XVJ4o^{T({E>7!n~tey?MdJkQ^V)dG}8#Zm~C~^<%>~Hl_J)3$Bnzwk_idAdYZ)X%L@y_3~eWSdimQI_ua%0xT zyrV23b_a~cmYhShl*iQG^gfcYq2G*o3l=S1xq3U2(BJjDHm+K+V&$?ifBN$$U#4-S zM!5BU)=qX1*Cdy?OwD??PjA)B?AiFy#Obrcq*n+tfjBznb^E}`QpV(7LDrhR(JKb+0*NN zp2>JcoVEz%p7Gwh_B)O5el~1m`d1UDE+^Pn)>qz!C2C>Ayn%1L{>EExb?;79$JtWq zw~pG^zIWrt@4fr3dZ(uMAHEnoeB{`#=dE40fmPUsjcXUOUY+%cdd+;@d?WN08P~=d z?af3~5bd$IYWMi}-y`Qcy?TArum9k*(UX?1TD?}5!K>z~*>zL@`0HQ)<~RTO+uu=X z_qV69**qV8gq5xS)BBRM#~Z)@@RNQ6ho{e9vTVi5l`B>(n}tGr!h2S-`K#7n|0h}B zcxwz(qln7Xp3>brM}P2pGWPgw&kz3e@n-{uOqsuM(Gpp^Pgj#eOL zU;X+wum0|}q0GZWJig_{Rle%O56If%)i?XR|A#++`o+kZbJ&`&!I-KtyszH1e_{Tz z<=3Ch+RH0-UpBUba>=H473~<8LcSia{=i|y`FGZRlJbWSdc6AT>)m_ye(#STrlOo=+cG6X!qHNG@qhl$FMjc>4NXLm zoYGJHis^q!N$K_KtF-d%Uhn_mld)*FC$ec}`|W-7OY0Y2pG)9@RAtM%t$f?2!GHc^ zpWnXv>T7Sj*}eDs9}G$tJ>0l41kZV&{~G3=g+#t&sU0N}pT1>D-#@?qyWjrycW}Sm z=iNW0YHjA|R5irw{~FA1TxX}S-P^|NwPy3g4^v(j*4JNuvuB@oKSxiaMzjw@Kh(dM z^-C`qUj^oHOSO(|4&Z%fRd&J8ET)2WeMmtKV07z>r(H!wmvR#iGw6>y=)sB?%Pldn z!soyjtmuk+T1+`ML(cA~4n~QcaZ3wA1Y|Z^V6dW7d2Csr930|SJ4y>c>lC3q>YzpV zRC#+DmOi6dVzfmbAwN|nsC>~noC>S_g)+Y}7-diuf%5N`0p$MVI#~XlzqmGWt&{5* zoo?aS7hC>ZnHl9biCgeBEnd^49mIH3=`&nQ9&teoRy>8gZg~xqAc5sUO$nB%as`Dr z2q-ahTCfa~cUnzO2f>&3QBJAF&YN;%`a{A=SQ$}tI9b1xN_JtH~7(!;7MTygVQmdXz09LXyz??xa=7bBML zk}#A9r|iS6S=r|wlvh*|j87gnAi*t&>6Tx~at?(LpLtLotdN0(1e4+yY^_A@FfiWz zS zj3P*{iLLVPW+`fIKT%LzQVN4#(XE4lw{r^&cQixu>3fB6lv4fEa=(^yt~jz0$#W+w zaIh)!%)P?GA}OLGhDjADNB`Dh)1PkTy{zED<}-KiKPW6JDlR1%Ut8|!aCup2sZ$0Y zAO}{JS9m0FusQSe?Sgy4S?bV1ibq1Yw79s$E;Y+|H8;@@rN<8jGV4#>ymJ@E!qRej zgGRaKrNyeSzUc0?n+1gxHMNzLT^8VZAOcpL4`#L;zkc%;dG414%j?UE?%%t2zu;od zk=&E#a3T(e8!Jo2Jpks)YwD1l*?jc!wHx2ux>HbG=9E7!D=oZNP;l>NE^%*%vyRm9 z3g(1zo*_66cn@-9!O)U@@$!{xH*Vc2!gH5f%`3fc6*ONyj3cyUVitMT+`XZSvQw;} z4>f0Iow<;A`RetX4=mbd;+%U=-334A;C|vF!v`}Dm3B6o>Z&R$aYC-BxQbhL@!{Mv z=P%}8xmG}jFc2jr+%>oF+&U!-FEZf?{-6`j)JkqUukfg1>DrWi;`G_`d6#c7G7u}w!$ z9=yPWs0~)s+!JL@*5S0w)0x8DR;^$uCtRnL^vvgc^FwPUAcPg+VyK!@>QO9oE55fpp2JlYn3={*OZ(& zb}Wmf`hJ)Z{A~WE%j$}EIZs{mPVa?x|0CY1RpJj_S#bPVj*K3YLkt`0xKcT8OfP{59Wk+6Zv~!D8zr`ufA~~`FoHm2z?W>mzewy z@|8q>`O1_pKjj}Of(!Zblau5VkP7BvQl@5lmt(rTneL zn&H+PBi&1dzpp0>T}inEYeEMZ9(s65=~^&_viwh>wS!l#wW&(RP9x z*SRw{Z#?S{oQ8iSL@U9>pBk|flcU$|NyNHQfbeAe5CO#Dx-BWghnmny`?6&I&~MP8 zb5LnOVj>6}cJ|J>ggpfyaN35y z4k_9*`fvQl;)0Eoy3OLs6O4FVC%@viYS$jzZ@>x>o{YbteBn+=dPWZrKqDdKONax& z08f_`E|D@`F7-hEfUZnVj0>=6lM((K@db~MdCsVtIt4%g9pw)&!B-bM8xO0-XkY}a zKLZC20KY)MBC-K|6YI0!0Zh!mBs!(5(Rlg^-=KlEKvOJ;GwQJiMA!t5;AmGuCB|#a zJ2l87(5-*?%P-O!yBYP^4OrGeJFu(Nrm2u>XSm39cQ$kYQ^*E!g4oC&?C5V`!u#mLiMwDw> zyOwU+(+0*o!7_;Om?HjV0DNYu8Ld(yeRA+Aqq7)u^JWwc$3 zaABd--5ftP#^$_MH`3@?-zHw8P*J)K3OVy{4jkug$N!+q~C?7 zyqnP^1_IrqlMAS5f2!uPKy|>Ax4$=#E|SK#YnB;L8LefbQ-@2#ghlW`-2NjT3y(C4 zR!v?T$L?#M@CZhQ8HJMY`cGnj>1%js`dOGZrb%6Ds7vFCsuSRgB{+OdZe;EH>emkdA+`Nza*3Cx ztU9c98M(E!<+rkCi?vQ{l!RywAI_x4K*=!BG54P`9?}ef>ku@M@mTlRt^o&vwG3Ga z7(`fN5F-y~Qk!YP(QY|dCpQ^8vSM}~gz2L}dugA0Kd!NI^B*DRdhvPOgz z#1%CpEFp-5WR3>51Y@1~Rf}*wDb;7J2(cVSu;~z+v2q6v+5ip$27mlQFvnXFNEli# zU@s9ai$}mkKvQ5**k)VB_BnDSP5`=(h~s2NI+3z!U>jq>1ZIq2!L*OirfC~E7y-ly z$Zb27XIpy9Xe|;<(pGH9shC>zkZYdp&w@MSj1qK82E(N*h^HjkrU=|Vu9A%P*dntG zl|tAMH_uQ8*BKn52U4+{S)@yD=^bJ;!OpaX1Q|?!O1$BZbVvjKGHrxSe-Idqw;0Wk z&{PKm(EYIB8i4s3%en;zp{7FvWcLPLBkW#}rC8ntrAKGAc(_UOkb0mh$Y+fZq3mG< zd2+Yf{P{D8-$&^12;FrSEm9<#jIBemH)tnrucI8@8zPgy?TFBXeh!VH^z+XdHYRj! z^Ox9C3J(r-hIqqDo--H{3QZdk@dz01kgbv4!o`Cp(~vRZUCZQ?#?lyjcr58AP@_X; z$H{|y7 zy5)*&oCCf+Z#FpKlCvcWSeTtG&?m$dUy(6#Ak8%5NhFJnu*0Ak&F*c4~G(r)-5w9VWJ;5 z4!0p-RzsNiN5dh*?(a`PZ#8Q=uW6g?IMry^26J@Ns1bNg;PEwb#MT6(`N*7^ynN*; zi^(~CyiNqBhF>!&kvxw=AUv8hgaQ%gbOBw%#pm5onaX9I{rxhU*aa0?=ZDgdGS6z={z-l&f8h_SS4P zwJ>;DqWN3HX6Id&kAPyB@qTJnK1YjKzNo2_6=R4D8gs$qoVn}E&IS~ z&jaA}H)|Teg-Wz*Y7qNNMVB z*MZUv-!{YBvIBVFM+ZUaA;kEQO}`7vk>-vrLrT_);kILfzaym(W1QumWLT&|ZT7a@ zK-+mv??@x59@wW@{`oiq@JwlAVK<}h!d}7ep`Fs}Sg&ZTcbIh@YY93cyk+Z_hOS2I zS=n{Lham=0Vh;Nyk-?V%+0g>1Xx*{B`_z6cWg46)c-RNJ#-ffe7KawJ*Y^&v2aPb8 zk@g<)4rjBDfJaPpeq#)$Md$7jNFMwaBWdy?c3hE<61*QZON6xf+^%uA`#3mO2tyDd-xDAlP;nw3~^zxYS>~6 z!Dtha(9X0EdD$&b88lzC_-co%dDs$S5i$2;-aF{PI;r0o!-C3AQ4F3e}rg;)jmITR2}o4tdDyx`f>11pPkHf^*I1Q;gk zpx*$ZjHU~?>)v+q5oRX6N*bm6$9QLB?NVUcVrs*%B(1j&bRv8lWw*II2IHBK0(6L0 ze8??P@~jcW>ZbMq4kdGIe@ryhJ5(=8K}s^QxoO-eN(*C5CO0{c#v4k8VUM%d$A}Qx zFFe-%&XUF}+@Vx+Unj0HEtbR^o|u0En&cgT%G_(#RFPJ6F+QCT#1xBo$DT7{C_1pu z+pl|u(DwsEzr+h3E0|4=AY;v zOfeZUZAN7_?(q-vioS&*mn3WVvvTL`@pE}GUsOBWff44vMV)f5h_XnOOuod;9k$35 zLs{K+`7Yj=AD#8}KV-tNo1m^W|l${fT>jh4#UwGe@%@1yLQFw^sn$TZyq;sv(0VgZ?m_1JGlRX*tPAQ z{-p%NT`+2~wKcHK(f3QI9e!Sd(XwUuB+QlEtTFmN3S)2=Ag^6GKx{ulV9lKPqj%t}7Zt0C2r(m)$Cy(As$y)C^ z%61rRZNtI5(L_R6j1&RX=03rBvE|qtc=DjZ7wx;xYXem;#K$}++hPEB0!-=&u@z!- z5TgBr=#!g6@j#mr@F--_Rt^DW0HZgARl`SnE<)NEKOpnG^y{8NWiP^p%EiJXns%w} zX@Qy|>Z+PB_fi1o)2q4GvMmJDA z0$f|5Ux2|8dfw=aVC416N-sA}DTS-hBlgMiBIiKrwh3_TFQshY=F(V4t^MHcx3g=TR~i65rc!t<93I~T>kBhN-jJO*lnw8<&8NQDb66sX$Cfp<3k&ibfo) zfk>ffmxU@wZSk6lCKcr{t7}b5h|p4!;yc@{*dmO{mSjf*8&IH>MTr>!A%;^D6d-4! zS_a-CCPIocVbh_D&^l{cMnGs1pDE1|lA9?wXhPwTC@37NY=dm(nn^&X@d$w-z%{yO zjuOh$gp>f%X#!|*aX{LklMa}4l15w0Ois|WjHDQo-X!?)Sybu?c7&K5XY)LgU{5cl zGY<{&rfBpvfxq@rtfzF1fW8TnL~SA*XH{orsAK`GOYpr!D1V1kaeV)b@07sP@|}`o z>F*=~d;1w|G0(_Xf_lafZGSv?aeQY)zP*(AXSwuJP+iMcjQ&ct)#S&8zHMFKe1bMP?G~serCjV{FInf3B~nAu#~vx z4H|L3(J-n826P~Wh57?U)2<(~r#WC4wW)(TV9qps)sKwO#6qKbC>ppSa`=k`^+?#{ zOi5T^R1S1N50mp|IE+Ss-rUsm#vdA?jDhD1NJ1M5;+QquOTGPy(Ox%d;HbltFK}fM z>eg4d<5B*m3Q<7Yq*5;}pNQmR62D?m0!pl5D6>+%kvTswLMsQNDwZR11j@`BZl}6a zPu7F;-!QBL&`vagl1CEFG&g;BDG#$eNk#k3NjfMp(NPT7$L6>rH}Md47f7n2E(Z}@ z+UKOUq7+OUF=8wlDYtU?KoHfmC^ne`=)vS@o6`Uj9FC?qLnXyYmADo!y^OW}*8_tZ zxy~j`M$L?Zyd9+@|2yd_y|iV#;>TNBN{j)bR~$NQXkb`?TD=wG)|7`r z-5TUD>LQIpk9q5w$uvjEXmlYv2x+JKJL5T(X)v8AROx!5dLvIVR6D611?><4Ym{+z zr1%MfIMI}559&n!300cvtwgH`91HB)&M_oNL1-#dhmSf17RG`>-E`qHcHaDQDAgm- z%?_Oq=@N%Vdn1qVPQ63{-nR1!4Xnd#g}kgBtbrZD!J)ZLrNbcG$n6fFa0STrS%JZ! z8IKKPMWm78;psU|KsAg&U%Z^KA+P3Z8DQ8)?5q6&>SJrLH!K_bW#v>|xI6Yh^(%%y zk;t_LMJyDLk5#lLoVM(86QTcgC)N=S)K+-<_{lT(8q-w`U_;}_u99PWYB!~hxD5pA zo#|iK0bOwDH~_vC5BXSd@;{95j?_hEFr7w9fj?(-b|k6Ns?P@ zG#(P4VmY!#?e%kzIrj`DJR!Mz_P74f2p`RS@KE9OC_ahor}y~fuNZC3*_l~44``|) zx5K88r%et1(1#$3N3jsy-FqJVjR7#Fu9|am5K8T3m$cj4bAui|ue7ihn^P9wvyIP?~Fj`oRUUMIo=-fSi9|AHrq;X zcL8V_b(!L$jrxX~9Xt#iQrzu3ZlOc}MzV=aXcgGGiW8^FpJHDLLbaQ7fTo~rq1A}I zi~7_~Z^uptk8uASH$$;av1X)B)F4R1W))XFjs*(kiid$ziYJFovg80AJkPytyE&V) zQVy>1_EL64T!(4{c!Y8m&$gr7e`w~&i>`v^n>#tsXQxd%+up7IT^2x(bh1OTMXhQ_ z%yzY{eJkFA+Z%XbYwH*)Np{B9Xg8bIvgh1eO&0-sqT10 z@mHUuM)JpObGG`oNN7G3=_@H%(HIkf(35QyRp1#OjCUjLk&d$H3bieEtG6YGt~!j` z3V|HPlD0(BY|@Asa!iDIrmG0()*5Q(Kp&w#pGg-mWr^Yir%Vo2E+}M` z@gJZJ({n-Q0d25}6mU0h{f5_8?ruHlq)*Z8Z6#}fq(ikN)$b#zZRf6o5?~-rx3&gR zC8O)$Aat3i=L;h3vZY-)wLF&cM7&T=Yt2qwo{bFd=H2%}htEdqxHFX1WEjUIU4aDK zueS1Z+AAIJ9d7{@sD7XB3zD{7X!wEAwpVK2c@3|W#|Nc)MPKa5tLMf%NWzh792~yc z%^ZQgE$4nKH+<90Q>8qls&3(QRoQg?Q4KT^r_r~bs|EhRjpO7`v$ES|h;JfCVF z^Ksxh52=$I4g#bTy19PCrfWavDYu^3t;2Fc(L=S~Tt`UUzZfko>({Saw|4Eiv;Sg* zao%2Mt@Sb+ui@eJ(9xpB7TOQo;zuDKOkpIEd@+U21M|N7psIzj zZTObWx^Ld|%3FCHRSIaLU1Swn4@~mg_tib`{$rj+ZEPwf#V(A6>fDoUFmO#2E0TA5 zU_o;W?zLd8EQ<(fLACGO_f&yj!F|-&L5+RjzzY>$VYi}0m*5~+3{_ih0i3bx0(eU6s8GbOX?@j|7F7K(h*!cGO|UB8@rzOI8REesT>yC(d_ zGzjlQ>7+;@PJt~6B|x0J4aL!NB!^QFxC>e)Pl)m-?9xvAu3K=og6Biae_{5~)6y79 zwjTs&qEq19_3kyYAPMUMkou$msKxC(3F>9qNc*IZAQo_zm0MnUs3c?p zXiIhYW!6}QdmA2r3QTyN? zf`;#v)P=c!LXRpMx#gQ{!Dnp>>s$Wu{d*-%+|BJ}ckgm2QMlp3y>@Q-sv7S07J%fu zLNK_(<#!4>!z@&bH(>S8=^E>{SJcKG-(uZ%Z{4Y5ITBL0ty_M@zZl+~+s-Yo5QM7d zjn+VC!$@F1B{-K!0H(K^oWcrhXM_MsQk#Xp9cVt9L9XKx4R&u_YqU!q3@PHCB_(L= zq*sjAc+lfz%Lqg{+5;M!qe0!pToR z;U{dw6M1t7Qlh_v)aK{aJh1({7X9mn!n_Q?W< zl*qPOZ*T?MG_q;6QNA!x(z$TZZ<@mbcq*}TXAbNF;YvFLCVo#RDBS)M0gN}|Q$XaW z_iz(T1j5QpYSX8Z#Rr(MoPQOJx)7>F4p~YHrBGi2Yw3g(V4)(;g#yMTLn&i}y^rw~ z&`~*UjA@I82v0#%U+PJl5M!I>zZt%zOyBx!%rLY_#h zc))0BD881nc}l9dc8N2c-esG^N(9}7y>J9vBoef)A&Ufv23vc@^}~xt2@E>kC{!T8 z5x5%w7f}-hx{2UT$-$yir?HiNaAeLf*`N!ayRcS7u}(yMLeZa~_TbSHdU$5Oz$Bod zU~>UZi5w<7wnl>HX{W@a3q}vvP_w?v;$Y`UT^{}r^hbInC_uv40n8NPn>~k(S@m{Z z*r<{SFiWJk4#Xdg-86UJ{FjV^!g;h=c=VC5Ql$o@l9GUE0v}=if|rf^Sz~GElg^(w zQJP6S&#YMyj)jZAZ#=j&MKFmpmQI907cm=%4?>L=FIoBnqwvx4=txk9pa?|@Aeuo+DEWe?}-HqV>4VBz8=L{+U=DMle_~W#rk16; zD4|OR!V!1v{;5&5L+H_hHg6GjF@up%wvw9Ht`&A2JSgOS`+sIUJk`N1g-56d4nl9) zCT){Cbs5@O`_SJR)x}zpD6e>Ms7^Sw3pljwod0gr9@vTm)U^MAI3?gJ zauj0qF6yCjoi&HkfH+l$4~sdQFjUgrwMV)Qtt8N zCr?3XIff%US%l%_snh>p)Yl*7I+6}J|34ZH)ko!0QqP>>z`d4=+~}1$`%gww*$JI; z_UyTH|I=tL6)t|BKmX50O9}k^yl~-Pa1s^Tg$oyXjaH}&E-vQ(i}i`RXy*Og2tB@p z!}-6lNWYq&|L?r?Z(joSusGd1X8iA=e)O;1C^`zZX18ntWC#u*Rx?OEsl*uTV_x;1 zt1cQKZWyf<-<-?Mi6x3$qSXa-p0jT8{$Lj_hsTQTS8T?u>=rZx1P2|p)|B18d^{&e z;H#kH<4O``6)SR>DEgfe$LJEu*%fc!vYTfKe6?YfQst~!eL5>);A&UnUqUJHOnP|x zBn})>Bu83djgM^qusI8#+SW#hIY1a2o&&t3;G{U92%{9JZLAdd7)Fg#0h)o}AFStr zRFf|!;fksPl?D=4N87H!7^Juj>({MCIa!Z?4n=8eH$|B{+oi=@i8gPcEH*A{*Q{p6 zUv*TflU}LRfB`xlPm3kxfX1=vW&CvZ>CnXTXjPOV0>J>snpHr10Pt9LP-5HFjw+G( zFVZPt<;vyDeqgjW91#!*wY};|y40Za3#bXtqSg};$O1qjq`f2~0nx1*^JFp4qSLww zlB%U6ab+}d(ITEU$D|Xa57z69q)AR-Ru(KEwdFuZ#0Iz>N=q?`!*Y~WlXzT-S|jxSy;&^*XP z;SuNSNfV(ruY*hjzGcQ0m}k)gU}rjbwVOTu!90s{q6KKDYdrDr*5k3Wc6LWi+K(8o zV~7>=s7E_gi^HBcBJ(V|JOg|l@$ZiwB{-I6QI`0vi~q6k;2X%3XR~k!A>uj=rD2I? zh&l+5`Uj(_diIlaHHt@3(PYUjj?SY-j!ZIIZ+{)R5`dX@9=^CpQyse;tnk? zwExV>D`WI8Kb0p@{%G7yQNVN7?A3JKPKR@3?5(kU^B^jkfWIgYqUxE$hVdY3+Axd< zQI38P9pXWx|6HT#;j?1M#}xkzrfDq1m~K_6X*9bn^)(Nxsz`dO@@XQBVy}S6-LDWknsVw?g5qsCy?M_*Iq2j2gTO)Cx{xLl<@rr_t{zgRP@7?V)5H7_$ivY=;qlf_!+xRDNx-D z@;e+ZzRzA%-HR#sJJbmdzZ`a@9Dh|MEM`H>UC?cdQBCm6zw_VO0z#Gxb?5(~PWc_U zYv1`F=r9ZT<=pvu!!HTk4e;%S3f0}%JO4np-s^nlA5rt)jk#mqP68qOu64(`{ZFXH z@7}o+xcxs-QQZmN{$~`KT(Nb06uAAbLbGpkVsZT)>o%J2rUG@FS=W01Hp+50T*A@0 zewChe51Dn*KZ|3BwJq^QZ2(R2lbxhkAY9v%4TE*RB)gpc`#njuNDacxA;l80M4}nV z`-4a-kx!Q2l{lS9o}bDo!TOkCwqY!c1w5GT7MuOVWOjxuYLhZv;do-7AfA`S6#Ftq z7t8U&FZoOpHZ}=zu(8S44~+y)JeHj(Qy>vEUIt#!e}9ZQIP-bVM+WegPXflC^M4|= zc%ShE0GQ9@c%K2}Smd8k7WU=8lk}&3Ij-z!pOL~y%@~PEo8!&osEPkUN;jWD;ixl) zBr&s#{}1e{y7>P{VV(UXk~{hTL~^XpFr=_CV;GWw>}B7cl;QY{gTzWP4oMjnV99*x zL!u?#igDnJamdi);4=>TuR-Hf$;M#u@-;yGm?#0mYu7AWW-axXspZ~^we}YO$n!=c zFq*8?r_Y$ZeDyl;fI!fxS;Gru1?$e0t5&VqBJcBEcy16xG_6Gz>ZS*%Oe6iFff1|y|M+|=x?qvtq;?8V}EWp>Y zuH7u2&En*{5e7u?HsN6A?cjmovZPk?TIVle*@?SYV4J_m+GqiBvqSB6ckRx8!D!aY z|AmXzptr^Y4QqjDh&dFV;eaW(Q z(E}jyu?+2$-_}mNYKk7M_in_^)PS6*E^iv!B=@$9+ZuS~X>-_8wwJ?cnjEou)IE|-$!$kB<2bt`Q@n{mG|GuR5;*Fg#!)YSx85#*oRSR$ z5mZ})?q8!n^7ii4JBBPdY1863Rx3C5xR+b_U8Ao0I8YCIkCAnRHN84!<~qkao$zvN zpEn*q&esq|vMnKOR*rU6#~nE)_Eg{`hYrLW&5!dtaH}K$I17mX#S|!w0~t!SOun`0*2Dw@-CFE%w(?ePLlW9%0SZY$*a0`sZKekyPokc zps{0

n)qr>xWFnWxTrXRf_$G_#A4jmN1oXRNbv=ghNyF{+Xyt%#O;{N&lQ*168- zopXNq%SMAHvBA*$hAtPZ^X|Fx(3@q~ae|2ZGv}QP=0)JxE(R}{oa_6t(InfEQ*2Mp zoVyTr(Z1k607+aZ_L=b63l}e1d47H%&%S70@UK$qY3qzS8$NeFFE2LVg6!w<`a7-8 zm}li=U`|iVH!lTAp^|*q&xEuj)&=ikUhE}Df^aXjq60b`(t8-^qD#na3<+-Dg~#7B zDEK_GIeBKjeK~N+&iC@lF>J8gQ5W63{5&V$zof2MB&mG=HswfT^6Y%)l6l#@VqfMv z-@gF;qM2vq+m{@u&Sguu%K;|OQ~ByrCqjJf%l0KN|ME*X9OUO)WVWsZ`Lkek^Ydhy z!>FK`S1p*OO1ScUf*}z2k|mX04L~)oD2j6PueJf24&A>Jb2WG+pt-$kZDnLW2N55jKCGoe^QyY$UA-2(8bgXn1~>0A7O3{tJ6En; zy&AaY647j5^{(ie+Aihi=jGkU^5~UQkE2s#5stRg#yoW)eEmTU9-3a`EfGU9cah8v z9PTBFV8*0+uJP=tQx|UDy7|qG8{a$(pi94Y)w=dHvN@N%Jb29WCKJy*W1cooaVOj% zq_^>ojTk;HaOes5(#2>|9c<=F?^F|TF%Pd_yLOe*eEwo6QC)WPzUB4LsMDcSCr_Rz z$GNrjUL4Ifub7wVH>RA4Waimcj>g0)gT{%GD#`Wvnt~vA)wmQwhygS<#;kZp$ zaPvKN-M(gCk3}|hg;UY~N_h9w$(sP|RCd1JMo>8a;gznWwn#siXWMmI*O4al;5m}5 ztC-xPHAR}{oysRZq5aPF8n+u73Ma zU|PEO>izqH8qayC9PsjoUs=EOrxRecrB$|S8<+O&WBty2t@iaddiL)9XN~eZn9$cR zK6=$k_CBaas|y>i#|`fE4_*__#@FBK)$5&41*3c5mz?DONq75K{*mt(je76Po^e$7 z-~G;ht?7-od-ZznbAhn_LVXtg^!?xbTK&o&_+6uAL&R#g@}uBuO>co?l=7(@tMjS) zr1ej){pL5n{&n&--lyx@*qW|hJ-54h&3e7WvmqiF&;A zr$2xAq4g&(o&#{||piu|DwLf7AY**PVlV&82Ni7B8Oi_8V_FZ<^iPfK7V; z_aFS;df)Hmyymaqbw}!=MQP5P>Mg&!dOO_nz4za@-VOJ8^Yz!Q*ZgmIj4TSXU;IqH zX}@K{(9-jrci#bRtWOW;b?-w?32tW_J%4se;4P)I_YqVnujgCN8~zermVzid{ax!V ztGo4fxJS?4y{w+z+i#M1p7&vy>_VsXcDmbdn>|{4_UhTA$J=ke<-F;=(Lm}n*^Ex= zt-1$DZ9(uJ-QVv1mgF5n^x4)P0$n<>PZtu+x6SVUTY)$IO)oNzZ*`Z5C=?t!AueT)nd%~DLPEWgs z+0#KTtGhp%w`KL!OZTgJA{FWgwecTz?HQzae+qAx43jume`wd9J`|^i_x5sLb&+V> z^bdOVRK4t;elOKi_3%mckMLrXJu?8<13GImQugqAo_X15(K~05fd3!~fe29|^CTG7 z8{DT;FSlopo|bf04=mGS{siQQ6~yE;H?{veLXQziN4{(*9dGKSdj0w+CPZvcr2M>*x0;TT z#F)FwPk$80%MZuvZzwKY zgTq4xeh3-lv0khg3HgQ|AR^cO@()z6dU|~ya@+^;!1j=?gS~!0#X%d_$O{shHRS2R zZod@7hd8P6yN1M|1_vt~yF&dxeyUor-5|8INe9bMIRCi>HEQA1$M zHa0F9FEg3Un@o~XW?tSOSuZp1E#5z4Y&^CT&-hJ7aqO2kSq^p_z%&UALWo`^pcKLQWXMdHA^`|e6 zE4OOnu+78Rm~S2P+uuI&n#%FfHT&7b(@(AC&3Af9)@#03u}J^bbN%T#XK{*gV1gfm z^4DHVdsPvw3qjDUKlN)o_qqMNPcALBUK>P~?^XBZ=OhCXJhKV)-s6)}b+R>5y=oB* zY~;TM$Q1(P*)Vag=C$Cf!IAcFdU1s?a^$c6Sy0J*<}F@#&0~D8rj1Pfjr((5qWP?< z<=u3B#jCGoj!gLt;S*kY-UC;&X~C7n8I+U$imu-$g}iYMt>H`a0{~~dLLt(h#Irx` z;^nl+AyJ~2*{{TLW&f{!raI-X*YBkbfm87l&D%D3pZdLUzPG@hwp;8y?l$y*R5R8} zr}|EI?bzfU6M7p6+1j{guf0$4zIJzhV87>ngnl?m@+adb!dr!#jAx<^oA8?AeKIz? zyEZ!StFHSD8@@^6z(*hWKDW)PQlFsNkcg5I^4j5f*ahh8sX^Zi1=aZ1bw!?0l$p-fxIrL1JG6N1bbw zuxazwCO(UI**o3s8=MAp8iOSi2HeigFrYtz7rr^R4HmuRH*LbGuwgs0VM>S}J19o4 z*2bGufzzUuTSQpHv?wDKafG{af8FL{aPA-`(BS)kXwgA7?DLkrR_5 zPY#rB+h)5f(cTD7kf3SKcHcI4YlP3}2`& zT@E!i?%LDR(zJ!#c(kdpF|fmFvLzdp+gR_X7cbr5&GKm{yezfXzazj{DDurlb@*Y! z*P)K`sk@K%$&w!l){TOIZ zwC>^{*T*d3fB#NfCkNZrfwWzYObgCh!FD&gH-ksBNrfm77fJDO^X}d0B&kCW8xAZm zx3$^XoyM29YT@nB9&A%bQg#P-xqJH_GE&>jsC%S2Wp`kgy7G__Y;&UcMQRT2_U%$1 zKV$^j{Lw_~p5`>ZDdofzXbVK+p?%HGseH;S-V^?|lxQMyfM)gLHo+-gLjz{l{q=avVT8|t$Kmcn|;EN85+#SAoaM!Lqt;a85O$E0xC6Wk7BKL-c zR1m5cg6}863cC>^E4t^RcI2V3+p^oygpac(@I&R^=y@cZII>%(+D)d!8obUvelhw` zII*uknUTkT99+~PVJMq~P3&kq&dzR>R zPe^oA?w1?USGnK#hTsfE+l3oRdBAuCg$r?6BzIb4W{BK?g|5fL$vnA}JalHb@$yvK zL$`9((1*Ny_Jtu0+>31wEYU2?Ec87ac`yqt6AAc=c501QYk{&+x=0e_i}eaS_#>2ZW+Rw)=(HLP(*yCgczq2S6>9h7>H}{xHL+4e=yu zHd<{3)gc}|T^E}eY(s}K-9=T$Q$Z3%7i*}Uoj}z^TlPGLB2K=jz&OI=9+H`s$uu8| zc96jPvSWlq4yNG_5K5vkg!NC3o1JBebrzaYnDsOavxYLShvomJ*WPrAsg|AdEw3Bt zD8^telaIj9%Oa~V!MDFP&CE~m?QXppLuxf1HyYwvLNz~NT>D_5CxSydhWj7jE_>hjlXE@s zlQl+FJz$)#@q}{P;gt9#LPnNvO}!zX{Nu;*iENLNh;vu3FJ3?dEIH=PB1)1qJprJV z-z!Dtj-^u6|KAW6;c+=&3FK_zLP#rny)vg-Q(RoM@_7y9=naF7&I1SvftBiYsp5o5 zi|9n&m#aU|Y2x(3C`D*dqm(N$OOZB;#Lp_Yp~ZK!lKQc03EyjUUa1gSDJQ@MBFLl_ z{m@>=r0ZBfj48R-IMXJwP!U&Z`Ki|m?Fxz@7&uFsBE%#>&X}rYy_oxLobYiM8BT7cUJL@T5d@sy9&X=%aTVrw)4p*fNR2ACRIb#`4)tG67 zJc;~UE9#^-p4?dK>7NaH)C#F(oSvZW8@mS>`z7XsC>E?*D$;4Mb$23Z*Id&k2oe)< z%;-0EATdHg;55>E)B~1z!gHa5?vqRqqMyw+XI+}fQb^)& zjk{!vZM1y4jh<^_i<*a^dx{gc=Qqn?YI6GEA!)pXLGu;(j1w(?h^VKzEIit6Exby_ zGyA6les-p@4fv+30>5$Kpo}lmNorh~Y_T3;y`-ee^Cl<92#-v;GiEx}889ME8qe$K z%6d;7qg)g#ZtV<(x{-5Z4y&h!=yQ3%BtQ!-wV@Gw+ljPv9 zTv0EYFM7S;F`R+07WXE%7eO2ApRMpw9$~`LLMD5%vSqih$~0Ed;@L?$;uUZ7bbF39 zJHi0D04t!Sm(| z`?t^$_gCZ4HMHicIVnaWlHX!g5%y-tv zWCqb9!p}v)ge)?Kv%N78M`wvC5}qkwv|it~S;w!@eSG)r)k$jB)h_jh!t3M$uS#K) zxw%F5;(&4FG)G+B+FNxdm9Ofg#lqW`S{Hfblq&niFP7CGFb*B!qzkS`pmn-ZPj3qq zNpH%@o33a=awG_PH+B&lKoQMFhzKgg^}4$D?YCBW)oN53OW~;(-O=1P77;V5*l(P< zhPI&BWRPY3LMJ_StfikylZCBMtCP4Ue#1tMSx7Lj5^yiWsle^l?fx}r7*StZL!jz@o``G zmVP`4{2r~9c|`ObTy-UD{Iq7aUQ^ns-KfzE%dN6hf$9S%6cL@v6kMftC^a_F-P}~dZR=pV!Cm9Cqp3< zR!;${()&GhS+U%z?_&?B%Hw53SheygtbK&S2Uhc}TDJyX zW#rVY)gZKx7P86Q##>RGY@cwH-O)=}nP8pX46yb5wj> z%~D1$)wpufTRwV&`9LCH=)OsO1q&tfdSmVakZe zYvGors@q6nl)=wMA96wHC3oObXCJ)0SH+5~~G*BF>?k+EdDi)L6AnosacG z$f*@5m3Rdl8Eo_(DAzhL0%1{=tzmHe1%$hvvx&Q=!mdlHmzj_i8p^7+YfPZ}*rj!s;pB%6Q8HV+g{iOLs;wO_) zJ2DrW#a4;A#Dq9rb9&LPuTHkzT#uFP^)$mCVLoX;@yUqS=P&eX&@C#Z3qnx(yi{Dh z+3RBK*VU~$&ar)j4Sa0)OT;6!7F|a-$?Jjcvr`eTx>bH`RZY!iOhla#78sX@KR+G& zf@B?xugQ$D=|{Z32RGNhi*j=9HspEgDO*=_dH6GL&L@JR*Y#VyG0S5Wm213yvshNz z)rp#h9tT~Jl7I2qcWwBH3H^vxk?W0$UDj7#QJa{EsCN)2p z(JM!2T3W;7f@LjG)Wt?poTLmc3mGfj#EFnt6g_A#;w8zJ=50%{&$v>RvK? z#N@onVW9_5qyL$QVGY0W#5k;S`nzNnS;fe#%iiuMe4@U*++RU}&bRxUb?wRVb9BUY z=8{d=Q9|98;MuXiQEyx2KH%eLpYE^W+T$Z~#U`b{4qUVr$Gy&DA-~?OW>bmF9t2nF z_5NBeJ^oLV#9T-;@MLOOMP_k)$*uViUs+aO=9J^bea%z-^_Y*lKYo)_^E?IBVtPJV zMR&AptyLD|ORl_($fleA<)223coLEk+34}!;r}{K>aym?bY+|iimDhK>8?uy-GF< zw{%I0abvsqK^Z^#d5vX1-uJ}+nL$8xYi&SNeo#t?*>L*lU{n}DH-72 z;ZKbCdH#%<5=?#0Tzg)k>K5r%EVlVBPJA+jfYJSEZ~WVjQFE}Lyf)&wH?R-W?X%|g z%`20d^h=0qg^(!{*q{PekJpSGVm|MX9OJq>MDjYoYY{S9>A;mn}; zg1=Vh+2@JLoi(X%+^=$;e-1ft?~nia#XMrRO8i+I!p|ud6-^x%7#SmK;}>X~1Fh}&_fY35y=@Vi%j z`O*tN``HUGzC3!WjuLCljL(`~q<07=`+MsNRI9-3J2m2|XMc_VXW}zYyg;3~<3_#q z%CBGk<;%bN&FIM(w^@bejNX~El09_RyQR!BqU*l%(o;WqJ&9-LehS{0QLiHjf9-c; zCt(Q)OLB%eGcNSB@onjrvg~j%aDCe^USQUXHKIAOFPHGjSmOIMI6T%jS+nSIY)x5%2K=B680X53*=3H@wBoQv_Joi8k=S!=)$ z@2tJ=y*nR;;yLJ*t+JDG(PMg~aHc-Voa}-z73Oxqf_G#e7PqH;@5~j2W`z5z#$f3w zMw1eX{JJ-~Qi(MMKg;>0R3RUWV7xVpi9Z`bQUwN?9AR05CjFLCl}gMv%_)6T^Oxyq zDs5V(r-b;?9qUlezX%tQ)M;6;&#!H!{P0vw{M>uB0^M3jwX;h69BuAQl89KL&- zC4#p&z7acD&txz)f_s4O> zcM2nX_iC5F#{|;XZT3joC!}5I9*9o_w+A4F;~-QAdd%)XPkOiN!Pnh2zJ_BK2zE~@ z78Y1t2;9eLZ(i!TOYHBv0lJPL=ZyGDNa;@PvXUSOFZ;9|g}*yp5~Z}Om}Fn<=aB(k=KsZNM}If_jM3B z=rm$^i}0#qZzw<;5Bu5&QA9unx7%rRPh93x@bsarQjZ6+;ef9_1Npn^PC2Pg_}da4 zA7AII?;3grw_#dR+kKd!;4VUX-{l}XH{0AUc*XH6djw8Gp<)WTH`wlU1lm;(WgWMW zW;<Y@}R{|Uk-Sy4jVU)$CX$;rLpfUnI)>hHGeW_MdOTLH(u+16j1 z1V?Q+Fi=WhQpHalZ$t18rLUnyqI-p_>JQz9g9E@*(&qL+ln<_5{aupm#D>Gspl*X5 zb=!1ZJpx5tcNCKNVB--{8j#7$OraWdd`~oT_NUtl>RBJgwzSfbbSetUfOYtyDw1Mc z_~g+3#$^BL6}s_F;Q>;S9@QQz8b|*axbqiVj~;1;w+5Xw+y@)>MS@W$;zp09;F2=R zEl}rMQQ+Ob^)}dtn~nsdz6c_4skGe(koz&3wU-WU6JCZ_)Q3AFKG9x?c4(+Bfl8bp zn$*$Dhqv8=dcUE?M^R{2BwKe(!?lIb_q}`sIIpnxKiG8C0S=O~oFE1*Sm3 zttnyEhFRPxv_Sxi5lDOc{?u?^=;*8efI=ANJ&J`zq?C~X8oW^0CIb?O!!cgTr%heE9>RFy6nyo{hr zw-XZG6JoCgLpB&x;XjF^+D=KwfeTGSdpxAbR_*?iJ}9Yk+N4De2wpkvNz_CL-gRCO z0^WPz=RnD!0hJFzh~=Q+kgf0XMb`lxPU26YgW=`}wG)tG zs5>-dp|$}i!LjfR!W{|=ZVqY<@hoDHao>RJPW+%sysifPd*V|BXCzpvp;~}@5gqD_ zcOcGPa97eWEDN`WCca)#PsrguktxnkaF2;oQxq9Q5dp0h-f8*&1zi%Hhzu~kjjBSH z-^t*n4>y{YiF?#|lJs~bgzbhTFvdy7B!x{$Ccc2IUV0dRq zbgu8v#Qb5y0?F82-^0Nv(-z&2p+r+do{Hj%L|02j4$J=@anwwSDW-2wlJIVBrATzL zei+aMSPlCdt`tLopSk8s1rqU%esE}~#%#IroUJ(O@cezb@+?;zy8NwNvUC1cA*;ou zi(Cne=4I1R)NiDqm?^3^-_z0LbXYmQ5*b7p4VI8} zy0^K;C9ERj`9ubC#<{ajVO%&fIL>`Dg011);JC#2+!kyP=M%n%!-P9=C+3Lr)5ohu zY!v5N6H>;z6Phquob8jKDHGg@doeRD^iOd3Ty-ZN{#GXrwn ziHrXN+pN62yK~k4ZyIV@AWx2dI8)4Xa@E3b8i_@d_^5XH7WYX?7-P+=GczUy^Hd$T zA5Y07FFq+R|5HplBlcv1r=W*!PWDeS^Xy5gjnwT9(OCkMQzxk{Jguy zENV4rGaWwi9nutWW7IOdzW0dL&ztrX`T7rlz^@KZF9mbnJ&H?p>{h14r%stV)juWe zO;ru+uplcC1W6RXT8HQ6BK$0F?++rBZ ztpYy~_OxKWTY!PKGs8jJZ51SnLoB|W^GRFZ1)Qp+V4CWx3DqIiakXb~#vj!5G&gPexxsY&_ z)?DA5)Y+-Cf-_yThi17m2{>ZS^UZbV%=MFzMqR=SIZGIl`M!C0-9=~c*^an7 zoud2;0t?kfqEi+7ip<3W7nuwFi>!ri3095q64<;}v7hufS_%|pElOYPE@~l6#S(vs zS*(g2uo5s9t;NA2RgcAOsc(s|WI(Z56e}*`KjFi6b2=)s$w(==W=ZN2w-!CnKzUyolG!pCuuiF365z)*I@s`6n1}}Ka={x^7RQ~;S>7%P zM(s*ph4A+8DRM;Y6sypL;z*gxYaceRJ*mG;(opkPIz6SwyrLt7KGXXHEwMSK|+?gbxQ{# zb~5YSTEvyA&R?t6WohdycCb}Q>zq1w%~^J^)z(s9oxA*4wh@0D8Hx?GyEO7QM&g}1 zBsi2niHE-@nYAK3sYP;jkPU53u-2(jhuF{Fk~_CH#BR32rg5s)&m#u)L?Wt9uW^^2 z{3}CIYM>^8%)zR5>o2m0eQE&~tX6BeKVwr%dbL}%iw&(Cu~3au?L*{{sH#5278ar6 z8h>@5O0B_Yd^ffEtAkZ4%ntTp4J7h`S?#ZKEAa`Gc)MDsr103qE_Q8susW^EtW@t0 zHG0o`GG7~Pge zlnMX)L$)Piu&9sy;P|WTN{4#iV>_#^+Z>e;6Rm!@`F52LrZ+OEbs2_T6|a7~`2-*z zh_I7RQI|=!2$bAPAn1&*DqhpjdVyn1y+uYk8PY!NE*u`25O41nKX|7B^i!p;GEr5z zlcrRpkm;^(<11hN6A$b6$+t zK}xJ>tIi}J%7JKBsinkQLh{nbWM<0xkRqvyAen(m6Yw|{ypL2!=6%IrX-+HfWr6a$ zDx?%85h0P>z+Z;duF9G0l@(xCsr7snv5YcBs8b%KBqR|8Y!X=*(xkG~@_`6@=+jb( ztBZ&!MTBp#(QUvfiRKFJ)DAYUCBD*NS+G2TP|PO(x~wcM&pSsZEN3J;u(;i_Qh!+* zxc*AjfSbDF65(K5OZ>D#YP8DTit+#rajOVg7!yHDNeau`Df5@7NUw2fo0OuI1eOFz zw*`ZhUn!g1O_7y|=*gm7nA`!1^^vPSK4&iWk#3VN)i~u& zh1$%@M)JbGhBkwP@I^2jFx+YueOx33fg)eAxBWpDr_2@Mf@0re4~5;%Mp)!W%wf}I zp9m4k%U81Z!6)@0fwS2arBp#=TN3BN^B33)ZP09*;vk-*?T!e~*agw2p-cxn3p}gD z4=K$uw+@S73s$JPz+JdNN;S!~b*XMyNSqOv^K5cO)F91qQD1_yE`mawRSz~|T0G$B z!g*s*vss*gb`Hh6t1n`149ncs6~q@J<**=#w9e9F>DCJKO8Dv`oABzfbt5jJJEEn3 zT1Hk+yp050NLv?XQkxBVgao#346|YrcFMy2?k~*b1tg7@{`Si%-Eypzd8f`oQ;U)k z=qBmcRT9kWm|NOJ>?U}{yt@IjVEFSjNLh-eo5>2*O1%w>pt}P`gxhhQO}Nt^H6w1@ zB|ap(d>m8V=taIHyYz|q-1m$`S2UU$QD@nAJJUG2s&nkS9jOsJtj@FTwg)0^IKol! z6E@wpKqSDa?h2bO$M@8*8@=(DSoTJF076j2QOY$~HW5y@A$9s&SfWLvX%xXpF%b%1 zVUxYcvmZVG4n8N(q(suf>H=So$AXb`4z}EN1|k_`f65LUYlF)X=8>{`c)m{aGdQ`q zk>1B}eCk97@SxahiMLKhAf)hj>mH+|)Q~#PZ5zeAp>Rq_$w%H5cGK(W;dK2JYimFG zNiT<}3&4yi`w~_(+zN4)4FQ0yNZauXH-rV`kK7ejm^`F` zarOp5nXfvLETB$Hk`-}#KH*D{R$+n>x|m@EAxUrhdBf<5V5?z=-0)Q?E;SPG_#{e7 zUntCf5kmb$Gr{2m+rzeoVaFh8zk{(uD12dnahF(4^vV_Ei|$P0ZYUWSmIk z!$PrihNdsmcrY?BtO$emfHd~{K;wS7vh~~-$qe^_7LE`khtGWK$qd6P!r|>8K~D>( zMI>}tCPbCN!#!>Wyb8tWr$=IfM|hhI>P|oOH6x;4rPW1!rO8?``EW&Y$y=Z;hU*beEz`u zFkx;8M@PEAT!yuElNhF9&C;M*RwONIwkZsDp?`qF>`00vbn6v{DvBCvd(JLdV%d&& zD}G^o-YrY4!SCI2RKr1xc8tj)%MW_Dtg@UG)hFg5Lx!->hNbc_4BLnyEVQ9%9NqJ= z7#YG+%NpQWn27V5v`h>sh7Fb5Onkj^bpMv&T0`!FIWUy9#~QXTSn#$i5FlCDk zKS5m{BPcp79s3@+gBC&mJz!!QGbEFkGFZ4^l5+c+i;0OcvPOMt`d9rOIDK%5$;U22TH@r@@AA$!z%2zXR(B7fJrV|C(NuR_I~;>26^CA3Y7ued{!{;YBrN$J&Zry4LtqFP6N*qU%yAbKelVG`J?IR&y?hvlk0!D-u%K}#`^DU zH|xLdZ~FE}4+zxPUo#G59rPgobeF#U%V@yJ)H54#;9%k9z7~hf4bhL zLoRIY+q+Vptbz6Xwv%=sH;{LbYjR!W6_P%I-$>G4An#{flOvPYO?o@ONN#ZJ(f_P6 z4a3Mw{<;ukJUKCet(z4FY$ zcazWWC$Afl*Uic6_T+V^cb)Z@-gPeVf4qN;xbYwU`O1+O*-A9f)&F#q`T?xTZXeG9 zp8Y)k`Tyf{z)1XAo_Gwqqty5h3}cI};&XbH`fN_(2ga7*|A#bqR_^N0R}EKd-}mu| zbVpmIMbKjo0hhi!wH#^<^Q%9Atta)#&!o0f(gsprA~pL~xhekKIb$4E4W{ zh|20FIqOz(5lwG!97Fw_FvVw~W3q8M0o8l>7k@Ei_%fFaCQ&lOy}vh0#eb1}ei(K6 zyH)H$>aAyY@Tox%dZerHi=OwzMVzjX?L|wO%xmFeX4iY^ACUf#nru* z>MNv#)Lz4@Ktw$t2qo7sfks_`4kn4 zA6fDb7KGaPg>a+-w-a)7{0OwwUkF4hK%shJuW>UkF8NjMkOcR+ql24<5+$uK^jT+{Tozf=R3jEQR)sX^hZU(LS*BDoZ0# zl*XK>W$~<%<-xUxt6sR6>Lt@-Y0s7I``Ee}yK!do-UaK zS8n9*nC_p=WlFvd1uy@dP=wjbhQUQ)Xe2pojF-R50DL}&)!+1m8N^286XXvO+}Dq? zFXa4ThSxUZ73*kkX-;0nyF1(1Hb#FIs-KL!S)E_*%~0_-zc?SyP>&wS{L*Y*EswU- zhA+W(4ELUP+Ww{4YB53dzBsoEs-F9NF6Ld2$pol_v;){%_g9X7VizR-gF*oNdjXR< zIh42Ek-z5VE8uo`aOy?&^#2z5;_{ZWdBy(W@2mar{{_%whf)9l literal 0 HcmV?d00001 diff --git a/assets/uninstaller.ico b/assets/uninstaller.ico new file mode 100644 index 0000000000000000000000000000000000000000..24278c229b7a3a797df4fbe921b3e0ad42a09309 GIT binary patch literal 196273 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 literal 0 HcmV?d00001 diff --git a/bueno.bat b/bueno.bat index 6aa7e4e..3e2ab0d 100644 --- a/bueno.bat +++ b/bueno.bat @@ -1,3 +1,4 @@ taskkill /F /IM "qtest.exe" -qmake -o build\Makefile .\qtest.pro -mingw32-make.exe -C .\build -f Makefile +qmake -o build\Makefile .\qtest.pro +mingw32-make.exe -C .\build -f Makefile.Release +mingw32-make.exe -C .\build -f Makefile.Debug diff --git a/install/installer.nsi b/install/installer.nsi index b2e29eb..9181444 100644 --- a/install/installer.nsi +++ b/install/installer.nsi @@ -1,61 +1,121 @@ ;Auto versioning------------------------------- -!makensis "version.nsi" -!system "GetVersion.exe" -!include "Version.txt" -; optional cleanup -!delfile "GetVersion.exe" -!delfile "Version.txt" - -;Defines--------------- -;!define MULTIUSER_EXECUTIONLEVEL Highest - + !makensis "/DBUILDTYPE=$BUILDTYPE version.nsi" + !system "GetVersion.exe" + !include "Version.txt" + ;optional cleanup + !delfile "GetVersion.exe" + !delfile "Version.txt" ;Includes-------------------------------- !include "MUI2.nsh" !include "nsDialogs.nsh" !include "LogicLib.nsh" - ;!include "MultiUser.nsh" + !include "${NSISDIR}\Contrib\Language files\English.nsh" + !include "${NSISDIR}\Contrib\Language files\Spanish.nsh" + ;!include "${NSISDIR}\Contrib\Language files\English.nsh" + + +;Defines---------------------------------- + !define MUI_LANGDLL_ALLLANGUAGES + !define MUI_UNICON "..\assets\uninstaller.ico" + !define MUI_ICON "..\assets\installer.ico" + ;-------------------------------- ;General ;Name and file - Name "MixerQ" - OutFile "mixerq-installer-${version}.exe" - Unicode True + !if ${BUILDTYPE} == "release" + Name "MixerQ" + !else + Name "MixerQd" + !endif + OutFile "..\build\bin\MixerQ-installer-${version}.exe" - ;Default installation folder - ;InstallDir "$LOCALAPPDATA\Modern UI Test" - ;Get installation folder from registry if available ;InstallDirRegKey HKCU "Software\Modern UI Test" "" - + + Unicode True Var Is_Admin Var Install_Type ;Request application privileges for UAC. If admin is not available, only user-level install will be available RequestExecutionLevel highest +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + +;-------------------------------- +;Pages + !insertmacro MUI_PAGE_WELCOME + !insertmacro MUI_PAGE_LICENSE "..\LICENSE.txt" + ;!insertmacro MULTIUSER_PAGE_INSTALLMODE + Page Custom InstallTargetPage + ;!insertmacro MUI_PAGE_COMPONENTS + !define MUI_PAGE_CUSTOMFUNCTION_PRE Skip_Directory_Func + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_PAGE_FINISH + + !insertmacro MUI_UNPAGE_WELCOME + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + !insertmacro MUI_UNPAGE_FINISH + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + !insertmacro MUI_LANGUAGE "Spanish" + !insertmacro MUI_LANGUAGE "SpanishInternational" + + ;English---------------------------- + LangString Header_Title ${LANG_ENGLISH} "Configure Install" + LangString Header_Subtitle ${LANG_ENGLISH} "Customize install settings" + LangString Option_Scope ${LANG_ENGLISH} "Select for whom will $(^Name) be installed: " + LangString Scope_Machine ${LANG_ENGLISH} "All users" + LangString Scope_User ${LANG_ENGLISH} "Current user" + + ;Spanish/SpanishInternational---------------------------- + LangString Header_Title ${LANG_SPANISH} "Configurar instalación" + LangString Header_Subtitle ${LANG_SPANISH} "Elija los ajustes de la instalación" + LangString Option_Scope ${LANG_SPANISH} "$(^Name) será instalado para: " + LangString Scope_Machine ${LANG_SPANISH} "Todos los usuarios" + LangString Scope_User ${LANG_SPANISH} "Usuario actual" + + LangString Header_Title ${LANG_SPANISHINTERNATIONAL} "Configurar instalación" + LangString Header_Subtitle ${LANG_SPANISHINTERNATIONAL} "Elija los ajustes de la instalación" + LangString Option_Scope ${LANG_SPANISHINTERNATIONAL} "$(^Name) será instalado para: " + LangString Scope_Machine ${LANG_SPANISHINTERNATIONAL} "Todos los usuarios" + LangString Scope_User ${LANG_SPANISHINTERNATIONAL} "Usuario actual" + +;Functions------------------------------ + Function Skip_Directory_Func + ;StrCmp $Install_Type "user" dontSkip + Abort # skip the page + ;dontSkip: + FunctionEnd !macro ONINIT un Function ${un}.onInit ; The value of SetShellVarContext detetmines whether SHCTX is HKLM or HKCU - ; and whether SMPROGRAMS refers to all users or just the current user + ; and whether SMPROGRAMS refers to all users or just the current + !insertmacro MUI_LANGDLL_DISPLAY UserInfo::GetAccountType Pop $0 ${If} $0 == "Admin" ; If we're an admin, default to installing to C:\Program Files - ;SetShellVarContext all - ;StrCpy $INSTDIR_BASE "$PROGRAMFILES64" + SetShellVarContext all + ; StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" StrCpy $Is_Admin "true" - StrCpy $Install_Type "machine" ${Else} ; If we're just a user, default to installing to ~\AppData\Local - ;SetShellVarContext current - ;StrCpy $INSTDIR_BASE "$LOCALAPPDATA" + SetShellVarContext current + ; StrCpy $INSTDIR "$LOCALAPPDATA\$(^Name)" StrCpy $Is_Admin "false" - StrCpy $Install_Type "user" ${EndIf} ; ${If} $INSTDIR == "" @@ -75,56 +135,35 @@ !insertmacro ONINIT "" !insertmacro ONINIT "un" -;-------------------------------- -;Interface Settings - - !define MUI_ABORTWARNING - -;-------------------------------- -;Pages - - !insertmacro MUI_PAGE_WELCOME - !insertmacro MUI_PAGE_LICENSE "..\LICENSE.txt" - ;!insertmacro MULTIUSER_PAGE_INSTALLMODE - Page Custom InstallTargetPage - ;!insertmacro MUI_PAGE_COMPONENTS - !insertmacro MUI_PAGE_DIRECTORY - !insertmacro MUI_PAGE_INSTFILES - !insertmacro MUI_PAGE_FINISH - - !insertmacro MUI_UNPAGE_WELCOME - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - !insertmacro MUI_UNPAGE_FINISH - -;-------------------------------- -;Languages - - !insertmacro MUI_LANGUAGE "English" - ;NSDialog InstallTarget Page definition--------------------------------- Function InstallTargetPage - !insertmacro MUI_HEADER_TEXT "Configure Install" "Customize install settings" - + !insertmacro MUI_HEADER_TEXT $(Header_Title) $(Header_Subtitle) + ;MessageBox MB_OK "Install type $Install_Type" + ;MessageBox MB_OK "Build type ${BUILDTYPE}" nsDialogs::Create 1018 Pop $0 - ${NSD_CreateLabel} 0 0 100% 10% "Select for whom will $(^Name) be installed: " + FindWindow $0 "#32770" + GetDlgItem $1 $0 1 ;next/install button + SendMessage $1 ${WM_SETTEXT} 1 "STR:$(^InstallBtn)" + Pop $0 + + ${NSD_CreateLabel} 0 0 100% 10% $(Option_Scope) Pop $3 - ${NSD_CreateFirstRadioButton} 0 12% 40% 6% "All users" + ${NSD_CreateFirstRadioButton} 0 12% 40% 6% $(Scope_Machine) Pop $1 ${If} $Is_Admin == "false" EnableWindow $1 0 StrCpy $INSTDIR "$LOCALAPPDATA\$(^Name)" ${Else} SendMessage $1 ${BM_CLICK} "" "" ;Set default - StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" + StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" ${EndIf} ${NSD_OnClick} $1 All_Users_Click - ${NSD_CreateAdditionalRadioButton} 0 24% 40% 6% "Current user" + ${NSD_CreateAdditionalRadioButton} 0 24% 40% 6% $(Scope_User) Pop $2 ${IfThen} $Is_Admin == "false" ${|} SendMessage $2 ${BM_CLICK} "" "" ${|} ${NSD_OnClick} $2 Current_User_Click @@ -134,16 +173,24 @@ FunctionEnd Function All_Users_Click Pop $0 - StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" - StrCpy $Install_Type "machine" + SetShellVarContext all + StrCpy $INSTDIR "$PROGRAMFILES64\$(^Name)" + StrCpy $Install_Type "machine" ;${NSD_SetText} $0 "machine" + ;FindWindow $0 "#32770" + ;GetDlgItem $1 $0 1 ;next/install button + ;SendMessage $1 ${WM_SETTEXT} 1 "STR:$(^InstallBtn)" FunctionEnd Function Current_User_Click Pop $0 + SetShellVarContext current StrCpy $INSTDIR "$LOCALAPPDATA\$(^Name)" - StrCpy $Install_Type "user" + StrCpy $Install_Type "user" ;${NSD_SetText} $0 "user" + ;FindWindow $0 "#32770" + ;GetDlgItem $1 $0 1 ;next/install button + ;SendMessage $1 ${WM_SETTEXT} 1 "STR:$(^NextBtn)" FunctionEnd ;Default section---------------------- @@ -151,27 +198,27 @@ Section SetRegView 64 SetOutPath $INSTDIR - ;File "..\build\debug\qtest.exe" + !if ${BUILDTYPE} == "release" + File "..\build\bin\MixerQ.exe" + !else + File "..\build\bin\MixerQd.exe" + !endif File "..\LICENSE.txt" + ;Start menu shortcut + createDirectory "$SMPROGRAMS\$(^Name)" + createShortCut "$SMPROGRAMS\$(^Name)\$(^Name).lnk" "$INSTDIR\$(^Name).exe" + createShortCut "$SMPROGRAMS\$(^Name)\Uninstall$(^Name).lnk" "$INSTDIR\Uninstall$(^Name).exe" + ;Store installation folder - - ${If} $Install_Type == "user" - WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "DisplayName" "$(^Name)" - WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "UninstallString" '"$INSTDIR\Uninstall.exe"' - WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoModify" 1 - WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoRepair" 1 - WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" "$INSTDIR\$(^Name)" - ${Else} - WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "DisplayName" "$(^Name)" - WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "UninstallString" '"$INSTDIR\Uninstall.exe"' - WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoModify" 1 - WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoRepair" 1 - WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" "$INSTDIR\$(^Name)" - ${EndIf} + WriteRegStr SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "DisplayName" "$(^Name)" + WriteRegStr SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "UninstallString" '"$INSTDIR\Uninstall$(^Name).exe"' + WriteRegDWORD SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoModify" 1 + WriteRegDWORD SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" "NoRepair" 1 + WriteRegStr SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" "$INSTDIR\$(^Name)" ;Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" + WriteUninstaller "$INSTDIR\Uninstall$(^Name).exe" SectionEnd @@ -191,20 +238,28 @@ SectionEnd ;Uninstaller Section Section "Uninstall" - SetRegView 64 - ;ADD YOUR OWN FILES HERE... - Delete "$INSTDIR\qtest.exe" - Delete "$INSTDIR\LICENSE.txt" - Delete "$INSTDIR\Uninstall.exe" -;!define PRODUCT_UNINST_ROOT_KEY "HKLM" - RMDir "$INSTDIR" + SetRegView 64 + + SetShellVarContext current + DeleteRegValue HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" + DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" + Delete "$SMPROGRAMS\$(^Name)\$(^Name).lnk" + Delete "$SMPROGRAMS\$(^Name)\Uninstall$(^Name).lnk" + RMDir "$SMPROGRAMS\$(^Name)" - ${If} $Install_Type == "user" - DeleteRegValue HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" - DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" - ${Else} + ${If} $Is_Admin == "true" + SetShellVarContext all DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "$(^Name)" DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" + Delete "$SMPROGRAMS\$(^Name)\$(^Name).lnk" + Delete "$SMPROGRAMS\$(^Name)\Uninstall$(^Name).lnk" + RMDir "$SMPROGRAMS\$(^Name)" ${EndIf} - + + Delete "$INSTDIR\$(^Name).exe" + Delete "$INSTDIR\LICENSE.txt" + Delete "$INSTDIR\Uninstall$(^Name).exe" + ;!define PRODUCT_UNINST_ROOT_KEY "HKLM" + RMDir "$INSTDIR" + SectionEnd diff --git a/install/version.nsi b/install/version.nsi index 6f5ac83..5e85c8c 100644 --- a/install/version.nsi +++ b/install/version.nsi @@ -1,4 +1,8 @@ -!define File "..\build\debug\qtest.exe" +!if ${BUILDTYPE} == "release" + !define File "..\build\bin\MixerQ.exe" +!else + !define File "..\build\bin\MixerQd.exe" +!endif OutFile "GetVersion.exe" SilentInstall silent diff --git a/qtest.pro b/qtest.pro index be04829..0568bca 100644 --- a/qtest.pro +++ b/qtest.pro @@ -1,18 +1,28 @@ -QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -g -gcodeview -O0 -Werror=return-type -QMAKE_LFLAGS += --target=x86_64-w64-mingw32 -g -Wl,-pdb= -v -LIBS += -LC:/capybara/libclang/x86_64-w64-mingw32/lib -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys -static -stdlib=libc++ -lunwind -#"kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -DEFINES += DEBUG QT_LOGGING_TO_CONSOLE=1 WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 -CONFIG += debug +TEMPLATE = app +QMAKE_CXXFLAGS += --target=x86_64-w64-mingw32 -Werror=return-type +QMAKE_LFLAGS += --target=x86_64-w64-mingw32 -v +CONFIG(release, debug|release) { + TARGET = MixerQ + DESTDIR = bin + VERSION = 0.9.0.0 + #QMAKE_CXXFLAGS += -O2 < Default. Modifying requires removing. +} else { + TARGET = MixerQd + DESTDIR = bin + VERSION = 0.9.0.1 + QMAKE_CXXFLAGS += -g -gcodeview -O0 + QMAKE_LFLAGS += -g -Wl,-pdb= +} + +LIBS += -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi32 -lcomdlg32 -lwinspool -lgdi32 -luser32 -lkernel32 -lpropsys -static -stdlib=libc++ -lunwind +DEFINES += QT_LOGGING_TO_CONSOLE=1 WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 +DEFINES_DEBUG += DEBUG QT += widgets network INCLUDEPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" -DESTPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" -VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" +VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" SOURCES += qtestmain.cpp qtclasses.cpp backlasses.cpp backsessionclasses.cpp contclasses.cpp contsessionclasses.cpp settings.cpp HEADERS += qtclasses.h backlasses.h backsessionclasses.h contclasses.h contsessionclasses.h global.h debug.h backfuncs.h ipolicyconfig.h msinclude.h meterslider.h qtvisuals.h settings.h RESOURCES = assets.qrc RC_ICONS += assets/logo.ico - -#DESTDIR += "build" From 667482cfeaa18fc00038192387fd5d1035764963 Mon Sep 17 00:00:00 2001 From: Hane Date: Tue, 14 Jan 2025 19:19:49 +0100 Subject: [PATCH 28/38] marshalling on sessions, missing override tag --- src/back/backlasses.cpp | 19 +++++++++++++------ src/back/backlasses.h | 11 +++++++++++ src/back/backsessionclasses.cpp | 2 ++ src/cont/contclasses.cpp | 26 +++++++++++++++++++++----- src/cont/contclasses.h | 3 +++ src/cont/contsessionclasses.cpp | 2 +- src/debug.h | 4 ++-- src/global.h | 1 + src/qt/qtclasses.cpp | 2 ++ src/qt/qtclasses.h | 2 +- 10 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index a0f0e06..517e10d 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -45,15 +45,20 @@ HRESULT EndpointNewSessionCallback::OnSessionCreated(IAudioSessionControl *NewSe //ISimmpleAudioVolume* sessionVolume; if (FAILED(NewSession->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl))) { log_wdebugcpp(L"no nueva sesion......"); }; if (sessionControl) { - sessionControl->AddRef(); - //sessionControl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&sessionVolume); Session* newSession = new Session(this->eph->getEndpoint(), sessionControl); - eph->addSessionSendFront(newSession); + + SessionThreadParams tp = { .eph = this->eph, .session = newSession, .isDelete = false }; + std::thread sessionThread(EndpointNewSessionCallback::createSessionThread, tp); + sessionThread.detach(); } return S_OK; } +void EndpointNewSessionCallback::createSessionThread(SessionThreadParams params) { + params.eph->addSessionSendFront(params.session); +} + EndpointVolumeCallback::EndpointVolumeCallback(Endpoint* ep){ this->ep = ep; } @@ -224,6 +229,7 @@ HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, D } HRESULT EndpointSituationCallback::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { + //TODO: New thread + Mutex os->updateEndpointInfo(std::wstring(pwstrDeviceId)); return S_OK; } @@ -236,7 +242,6 @@ Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ * It can't multiflag, it's that stupid. MS momento. * Only shows most relevant flag according to MS, i.e. 0110 sends 0010 */ - //todo: preguntitas owindows dword no es uint32_t even tho mingw mingas if(FAILED(endpoint->GetState(&this->endpointState))) {exit(-2);}; if(this->endpointState == EndpointState::ENDPOINT_ACTIVE) { @@ -288,7 +293,6 @@ void Endpoint::activateEndpointSessions() { log_debugcpp("Couldn't open session manager2, huh"); return; } - IAudioSessionEnumerator* sessionEnumerator = nullptr; if (FAILED(sessionManager->GetSessionEnumerator(&sessionEnumerator))) { log_wdebugcpp(L"sesEnumeratorBros..."); return; } @@ -488,7 +492,9 @@ std::vector Endpoint::getSessions() { } size_t Endpoint::getSessionCount() { - return endpointSessions.size(); + size_t sessionCount; + sessionCount = endpointSessions.size(); + return sessionCount; } void Endpoint::registerNewSessionNotification(EndpointNewSessionCallback* ensc){ @@ -507,6 +513,7 @@ void Endpoint::deleteSessions() { } Endpoint::~Endpoint(){ + //EPs are never deleted. log_wdebugcpp(L"murio endpoint-san uwu"); properties->Release(); endpointVolume->Release(); diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 071e0e1..511774b 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -82,6 +82,7 @@ class Endpoint { void unregisterNewSessionNotification(EndpointNewSessionCallback* ensc); void deleteSessions(); void activateEndpointSessions(); + std::mutex endpointSessionsMutex; ~Endpoint(); private: @@ -185,16 +186,26 @@ class Overseer { }; class EndpointNewSessionCallback : public IAudioSessionNotification { + private: + struct SessionThreadParams; + public: EndpointNewSessionCallback(EndpointHandler *eph); ULONG AddRef(); ULONG Release(); HRESULT QueryInterface(REFIID riid, VOID **ppvInterface); HRESULT OnSessionCreated(IAudioSessionControl *NewSession); + static void createSessionThread(SessionThreadParams params); private: ULONG ref = 1; EndpointHandler *eph; + + struct SessionThreadParams { + EndpointHandler *eph; + Session *session; + bool isDelete; + }; }; namespace Environment { diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index aafe147..f98369f 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -37,6 +37,8 @@ HRESULT SessionStateCallback::QueryInterface(REFIID riid, VOID **ppvInterface) { } HRESULT SessionStateCallback::OnDisplayNameChanged(LPCWSTR NewDisplayName, LPCGUID EventContext) { + //TODO: Preguntar + while(sh->getVolumeInfo()->isNameChanged == true); sh->setName(std::wstring(NewDisplayName)); sh->getVolumeInfo()->isNameChanged = true; return S_OK; diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index e9561f0..ad85647 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -191,10 +191,15 @@ Endpoint* EndpointHandler::getEndpoint() { } void EndpointHandler::addSessionSendFront(Session* session) { - ep->addSession(session); - + if(!ep->endpointSessionsMutex.try_lock()) return; + + this->ep->addSession(session); SessionHandler* sessionHandler = new SessionHandler(this, session, (getSessionCount() - 1)); + ep->endpointSessionsMutex.unlock(); + + sessionHandlersMutex.lock(); sessionHandlers.push_back(sessionHandler); + sessionHandlersMutex.unlock(); this->addSessionWidget(sessionHandler); } @@ -218,14 +223,24 @@ void EndpointHandler::deleteSessions() { void EndpointHandler::createSessionHandlers() { ep->activateEndpointSessions(); - ensc = new EndpointNewSessionCallback(this); - ep->registerNewSessionNotification(ensc); if (this->flow == Flows::FLOW_PLAYBACK) { for (int i = 0; i < this->getSessionCount(); i++) { SessionHandler* sessionHandler = new SessionHandler(this, this->getSessions().at(i),i); sessionHandlers.push_back(sessionHandler); } - } + } + ensc = new EndpointNewSessionCallback(this); + ep->registerNewSessionNotification(ensc); +} + +void EndpointHandler::lockSessionCollections() { + this->sessionHandlersMutex.lock(); + this->ep->endpointSessionsMutex.lock(); +} + +void EndpointHandler::unlockSessionCollections() { + this->sessionHandlersMutex.unlock(); + this->ep->endpointSessionsMutex.unlock(); } EndpointHandler::~EndpointHandler() { @@ -386,6 +401,7 @@ void OverseerHandler::updateFrontEndpointName(Endpoint* ep) { } void OverseerHandler::reviseEndpointShowing(std::wstring endpointId, EndpointState state) { + //TODO: Race condition!!!!! std::vector allHandlers; allHandlers.insert(allHandlers.end(), this->captureEndpointHandlers.begin(), this->captureEndpointHandlers.end()); allHandlers.insert(allHandlers.end(), this->playbackEndpointHandlers.begin(), this->playbackEndpointHandlers.end()); diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index ac4ecdc..9d43088 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -76,6 +76,9 @@ public: void removeSessionFromFront(SessionHandler* sh); void deleteSessions(); void createSessionHandlers(); + std::mutex sessionHandlersMutex; + void lockSessionCollections(); + void unlockSessionCollections(); ~EndpointHandler(); private: diff --git a/src/cont/contsessionclasses.cpp b/src/cont/contsessionclasses.cpp index b63631e..fd53e62 100644 --- a/src/cont/contsessionclasses.cpp +++ b/src/cont/contsessionclasses.cpp @@ -5,7 +5,7 @@ SessionHandler::SessionHandler(EndpointHandler* eph, Session* session, size_t id this->eph = eph; this->idx = idx; this->session = session; - + this->svi.mainVolume = session->getVolume(AudioChannel::CHANNEL_MAIN); this->svi.muted = session->getMute(); this->svi.caller = osh->getGuid(); diff --git a/src/debug.h b/src/debug.h index d4b9195..61855c1 100644 --- a/src/debug.h +++ b/src/debug.h @@ -58,7 +58,7 @@ std::bitset varToBitset(T info) { OutputDebugStringW(std::wstring(L"[DEBUG] (" + std::wstring(WFILE) + L":" + std::to_wstring(__LINE__) + L"): " + std::wstring(str) +L"\n").c_str()); \ } while (0) -#endif +#endif //_WIN32 #define log_to_file(fmt, cnt...) do { \ if(writable) fprintf_s(fileLog, fmt,##cnt); \ @@ -76,7 +76,7 @@ std::bitset varToBitset(T info) { #define initialize_file_log() false #define close_file_log_buffer() #define PIPE_NAME "Mixerq" -#endif +#endif //DEBUG /* Here as a quick reference, in case smthn similar is needed again */ /* typedef void (EndpointWidget::*epwMuteFunc)(bool muted); */ diff --git a/src/global.h b/src/global.h index ba5678b..88f2dae 100644 --- a/src/global.h +++ b/src/global.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "debug.h" //#include "settings.h" diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 376cdcd..07adb81 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -750,7 +750,9 @@ EndpointWidget::~EndpointWidget() { timer->stop(); delete timer; this->eph->setFrontVisibilityInfo(EndpointState::ENDPOINT_ALL, INT_MAX); + this->eph->lockSessionCollections(); this->eph->deleteSessions(); + this->eph->unlockSessionCollections(); for(auto sw : sessionWidgets) { delete sw; } diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 1ca937d..6d1ec61 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -203,7 +203,7 @@ private slots: private: //std::vector *ephs; - bool eventFilter(QObject *object, QEvent *event); + bool eventFilter(QObject *object, QEvent *event) override; void flushRoleChanges(); void changeFrontDefaults(Roles role, EndpointWidget* newDef, EndpointWidget* oldDef); From 59a92fa34b23200e5e964080d10996ebc3b5bf52 Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 15 Jan 2025 21:47:35 +0100 Subject: [PATCH 29/38] fixed race cons ep/eph coll, unwanted session state update crash + some minor code cleanup --- src/back/backlasses.cpp | 173 ++++++++++++++++++-------------- src/back/backlasses.h | 17 ++-- src/back/backsessionclasses.cpp | 6 +- src/cont/contclasses.cpp | 53 ++++------ src/cont/contclasses.h | 9 +- src/qt/qtclasses.cpp | 9 +- src/qtestmain.cpp | 2 +- 7 files changed, 143 insertions(+), 126 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 517e10d..a2271ea 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -48,8 +48,8 @@ HRESULT EndpointNewSessionCallback::OnSessionCreated(IAudioSessionControl *NewSe Session* newSession = new Session(this->eph->getEndpoint(), sessionControl); SessionThreadParams tp = { .eph = this->eph, .session = newSession, .isDelete = false }; - std::thread sessionThread(EndpointNewSessionCallback::createSessionThread, tp); - sessionThread.detach(); + std::thread newSessionThread(EndpointNewSessionCallback::createSessionThread, tp); + newSessionThread.detach(); } return S_OK; @@ -97,43 +97,55 @@ HRESULT EndpointVolumeCallback::QueryInterface(REFIID riid, VOID **ppvInterface) HRESULT EndpointVolumeCallback::OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) { if (pNotify == NULL) return E_INVALIDARG; + AUDIO_VOLUME_NOTIFICATION_DATA paramCopy; + memcpy(¶mCopy, pNotify, sizeof(AUDIO_VOLUME_NOTIFICATION_DATA)); + float* channelVolumes = (float*)malloc(pNotify->nChannels * sizeof(float)); + for (int i = 0; i < pNotify->nChannels; i++) { + channelVolumes[i] = pNotify->afChannelVolumes[i]; + } + std::thread updateVolumeThread(&EndpointVolumeCallback::updateVolumeInfo, this, paramCopy, channelVolumes); + updateVolumeThread.detach(); + return S_OK; +} + +void EndpointVolumeCallback::updateVolumeInfo(AUDIO_VOLUME_NOTIFICATION_DATA newVolume, float* channelVolumes) { //delete osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller; //osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.freeData4(); //Could've made a function or = override to hide this within Nguid, but back in cont = bad. osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data1 \ - = pNotify->guidEventContext.Data1; + = newVolume.guidEventContext.Data1; osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data2 \ - = pNotify->guidEventContext.Data2; + = newVolume.guidEventContext.Data2; osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data3 \ - = pNotify->guidEventContext.Data3; + = newVolume.guidEventContext.Data3; for(int i = 0; i < 8 /* Data4 size */; i++){ - osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data4[i] = pNotify->guidEventContext.Data4[i]; + osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data4[i] = newVolume.guidEventContext.Data4[i]; } - //memcpy(&osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller, &pNotify->guidEventContext,sizeof(NGuid) ); - Flows flow = this->ep->getFlow(); - EndpointHandler* eph = nullptr; - if (flow & Flows::FLOW_PLAYBACK) { - eph = osh->getPlaybackEndpointHandlers().at(this->ep->getIndex()); - } else { - eph = osh->getCaptureEndpointHandlers().at(this->ep->getIndex()); - } + //memcpy(&osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller, &newVolume.guidEventContext,sizeof(NGuid) ); + Flows flow = this->ep->getFlow(); + EndpointHandler* eph = nullptr; + if (flow & Flows::FLOW_PLAYBACK) { + eph = osh->getPlaybackEndpointHandlers().at(this->ep->getIndex()); + } else { + eph = osh->getCaptureEndpointHandlers().at(this->ep->getIndex()); + } - eph->getCallbackInfo()->muted = pNotify->bMuted; - eph->getCallbackInfo()->mainVolume = pNotify->fMasterVolume; - eph->getCallbackInfo()->channels = pNotify->nChannels; + eph->getCallbackInfo()->muted = newVolume.bMuted; + eph->getCallbackInfo()->mainVolume = newVolume.fMasterVolume; + eph->getCallbackInfo()->channels = newVolume.nChannels; - UINT j = 0; - //todo: do while here caused stack corruption; sus - while(j < pNotify->nChannels) { - if (flow & Flows::FLOW_PLAYBACK) - eph->getCallbackInfo()->channelVolumes[j] = pNotify->afChannelVolumes[j]; - else - eph->getCallbackInfo()->channelVolumes[j] = pNotify->afChannelVolumes[j]; - j++; - } - return S_OK; + UINT j = 0; + //todo: do while here caused stack corruption; sus + while(j < newVolume.nChannels) { + if (flow & Flows::FLOW_PLAYBACK) + eph->getCallbackInfo()->channelVolumes[j] = channelVolumes[j]; + else + eph->getCallbackInfo()->channelVolumes[j] = channelVolumes[j]; + j++; + } + free(channelVolumes); } @@ -210,31 +222,34 @@ HRESULT EndpointSituationCallback::OnDeviceRemoved(LPCWSTR pwstrDeviceId) { HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) { std::wstring endpointId = std::wstring(pwstrDeviceId); + EndpointState newState; switch (dwNewState) { case DEVICE_STATE_ACTIVE: - osh->reviseEndpointShowing(endpointId, EndpointState::ENDPOINT_ACTIVE); + newState = EndpointState::ENDPOINT_ACTIVE; break; case DEVICE_STATE_DISABLED: - osh->reviseEndpointShowing(endpointId, EndpointState::ENDPOINT_DISABLED); + newState = EndpointState::ENDPOINT_DISABLED; break; case DEVICE_STATE_NOTPRESENT: - osh->reviseEndpointShowing(endpointId, EndpointState::ENDPOINT_NOTPRESENT); + newState = EndpointState::ENDPOINT_NOTPRESENT; break; case DEVICE_STATE_UNPLUGGED: - osh->reviseEndpointShowing(endpointId, EndpointState::ENDPOINT_UNPLUGGED); + newState = EndpointState::ENDPOINT_UNPLUGGED; break; } - + std::thread newEndpointThread(&OverseerHandler::reviseEndpointShowing, osh, + endpointId, newState); + newEndpointThread.detach(); return S_OK; } HRESULT EndpointSituationCallback::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { - //TODO: New thread + Mutex - os->updateEndpointInfo(std::wstring(pwstrDeviceId)); + std::thread propertyThread(&Overseer::updateEndpointInfo, os, std::wstring(pwstrDeviceId)); + propertyThread.detach(); return S_OK; } -Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ +Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx) { this->endpoint = ep; this->idx = idx; this->policyConfig = policyConfig; @@ -242,7 +257,9 @@ Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ * It can't multiflag, it's that stupid. MS momento. * Only shows most relevant flag according to MS, i.e. 0110 sends 0010 */ - if(FAILED(endpoint->GetState(&this->endpointState))) {exit(-2);}; + DWORD state; + if(FAILED(endpoint->GetState(&state))) {exit(-2);}; + this->endpointState = (EndpointState)state; if(this->endpointState == EndpointState::ENDPOINT_ACTIVE) { activateEndpointVolume(); @@ -253,11 +270,9 @@ Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ } - reloadEndpointChannels(); - //todo: atexit into exit Gather ID LPWSTR tempString = nullptr; - if (FAILED(endpoint->GetId(&tempString))) {exit(-1);}; + if (FAILED(endpoint->GetId(&tempString))) { exit(-1); }; endpointId = std::wstring(tempString); log_wdebugcpp(endpointId); CoTaskMemFree(tempString); @@ -265,6 +280,8 @@ Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx){ endpoint->OpenPropertyStore(STGM_READ, &properties); this->updateName(); this->setFlow(); + + reloadEndpointChannels(); } void Endpoint::updateName() { @@ -319,28 +336,19 @@ void Endpoint::addSession(Session* session) { } void Endpoint::activateEndpointVolume() { - //bool extraThread = false; - /* - * Forgive me, for MS has sinned, and now I must too. - */ - if (this->endpointVolume == nullptr){ - HRESULT result = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + //If this EP is created after init, COM won't be initialized on the executing thread. + HRESULT result = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (this->endpointVolume == nullptr) { endpoint->Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, (void**)&this->endpointVolume); //todo: check header if(FAILED(endpoint->Activate(__uuidof(IAudioMeterInformation), - CLSCTX_ALL, NULL, (void**)&endpointPeakMeter))) { log_debugcpp("peakbros..."); } - // - - //if (endpointVolume == nullptr) { //why they returning 0 after dealing with the error jfc CO_E_NOTINITIALIZED) { - //CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); - //extraThread = true; - //goto initialized; - //} - //log_debugcpp(std::string("no endpointVolume (IAudioEndpointVolume)")); - if (result == S_OK) - CoUninitialize(); + CLSCTX_ALL, NULL, (void**)&endpointPeakMeter))) { + log_debugcpp("peakbros..."); + } } + if (result == S_OK) + CoUninitialize(); } void Endpoint::reloadEndpointChannels() { @@ -393,7 +401,7 @@ bool Endpoint::getMute(){ return mute; } -void Endpoint::setState(uint8_t state){ +void Endpoint::setState(EndpointState state){ this->endpointState = state; if(state == EndpointState::ENDPOINT_ACTIVE) { this->activateEndpointVolume(); @@ -401,7 +409,7 @@ void Endpoint::setState(uint8_t state){ } } -size_t Endpoint::getState(){ +EndpointState Endpoint::getState(){ return this->endpointState; } @@ -409,9 +417,13 @@ void Endpoint::setVolume(NGuid guid, int channel, float volume) { //TIP: There used to be log messages here. Now, it's a ghost town. GUID tempMsGuid = NGuidToGUID(guid); if (channel == AudioChannel::CHANNEL_MAIN) { - if(FAILED(endpointVolume->SetMasterVolumeLevelScalar(volume, &tempMsGuid))) {}; + if(FAILED(endpointVolume->SetMasterVolumeLevelScalar(volume, &tempMsGuid))) { + log_wdebugcpp(L"Master volume failed for endpoint: " + friendlyName); + }; } else { - if(FAILED(endpointVolume->SetChannelVolumeLevelScalar(channel, volume, &tempMsGuid))) {}; + if(FAILED(endpointVolume->SetChannelVolumeLevelScalar(channel, volume, &tempMsGuid))) { + log_wdebugcpp(L"Channel " + std::to_wstring(channel) + L" volume failed for endpoint: " + friendlyName); + }; } } @@ -525,9 +537,8 @@ Endpoint::~Endpoint(){ } void Overseer::initCOMLibrary() { - OutputDebugStringW(L"EPWidget creation\n"); if(FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) { - log_debugcpp("si"); }; + log_debugcpp("Not even COM?"); }; //Retrieving endpoint enumerator @@ -535,7 +546,7 @@ void Overseer::initCOMLibrary() { if(FAILED(CoCreateInstance( __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnumerator)) ) - { log_debugcpp("si"); }; + { log_debugcpp("No MMDeviceEnum. Weird."); }; GUID tempGuid; if(FAILED(CoCreateGuid(&tempGuid))) { log_debugcpp("Failed to obtain GUID: " ); }; @@ -551,7 +562,7 @@ void Overseer::initCOMLibrary() { //TODO: Uninitialize COM } -void Overseer::reloadEndpoints(Flows flow) { +void Overseer::createEndpoints(Flows flow) { IMMDeviceCollection *deviceCollection; unsigned int numEndpoints; EDataFlow MSflow = (flow == Flows::FLOW_PLAYBACK ? EDataFlow::eRender : EDataFlow::eCapture); @@ -631,9 +642,15 @@ void Overseer::reloadEndpoints(Flows flow) { } Endpoint* Overseer::addEndpoint(std::wstring endpointId, /* out */Flows* flow = nullptr) { + //This method is only called from the new endpoint callback and its subsequent thread, + //so another STA can be safely instantiated + if(FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) { + log_debugcpp("EP Callback Thread failed. Sad!"); + return nullptr; + } IMMDevice* newep; if(FAILED(deviceEnumerator->GetDevice((LPCWSTR)endpointId.c_str(), &newep))) - log_debugcpp("ay caramba con la hot metida."); + log_debugcpp("ay caramba con la hot metida. Sad!"); Endpoint *endpoint = new Endpoint(newep, policyConfig); @@ -646,10 +663,11 @@ Endpoint* Overseer::addEndpoint(std::wstring endpointId, /* out */Flows* flow = this->captureDevices.push_back(endpoint); } if (flow != nullptr) *flow = getFlow; + CoUninitialize(); return endpoint; } -Overseer::Overseer() : epsc(this){ +Overseer::Overseer() : epsc(this) { log_debugcpp("Initializing Overseer"); //Storing exe path for later use (mainly "Run on startup") @@ -669,20 +687,19 @@ Overseer::Overseer() : epsc(this){ log_debugcpp("-Initializing COM"); initCOMLibrary(); - //Obtaining playback endpoint collection on this point in time - reloadEndpoints(Flows::FLOW_PLAYBACK); + //Obtaining playback endpoint collection + createEndpoints(Flows::FLOW_PLAYBACK); //reloadEndpoints(Flows::FLOW_CAPTURE); - - //Registering for endpoint information callback - //this->epsc.fill(deviceEnumerator, playbackDevices, captureDevices); - //this->epsc.fill(deviceEnumerator); - if(FAILED(deviceEnumerator->RegisterEndpointNotificationCallback(((IMMNotificationClient*)&epsc)))) { log_debugcpp("when no enchufas......"); } } NGuid Overseer::getGuid() { return guid; } +void Overseer::registerEndpointSituationCallback() { + if(FAILED(deviceEnumerator->RegisterEndpointNotificationCallback(((IMMNotificationClient*)&epsc)))) { log_debugcpp("when no enchufas......"); } +} + std::vector Overseer::getPlaybackEndpoints() { return playbackDevices; } @@ -692,19 +709,22 @@ std::vector Overseer::getCaptureEndpoints() { } void Overseer::updateEndpointInfo(std::wstring endpointId) { - log_wdebugcpp(L"new name Endpoint id: " + endpointId); //todo: reintroduce capture devices + playbackMutex.lock(); + log_wdebugcpp(L"new name Endpoint id: " + endpointId); for(auto ep : playbackDevices) { - if (ep->getId() == endpointId) { + if (ep->getId() == endpointId && ep->getState() == EndpointState::ENDPOINT_ACTIVE) { ep->updateName(); osh->updateFrontEndpointName(ep); break; } } + playbackMutex.unlock(); } Overseer::~Overseer(){ - log_debugcpp("cum"); + //Overseer is never deleted. This is to annotate what would need to be taken care of. + log_debugcpp("jej"); deviceEnumerator->Release(); for(unsigned long long i = 0; i < playbackDevices.size(); i++){ delete(playbackDevices.at(i)); @@ -885,7 +905,6 @@ bool Environment::checkStartup(HKEY rootKeyFlags) { } void Environment::updateStartupConfig(bool onStartup) { - DWORD typeReturned; wchar_t regSubKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run\\"; if(!onStartup) { diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 511774b..5876b4b 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -58,8 +58,8 @@ class Endpoint { float getVolume(int channel); void setMute(NGuid guid, bool muted); bool getMute(); - void setState(uint8_t state); - size_t getState(); + void setState(EndpointState state); + EndpointState getState(); Roles getRoles(); void setRoles(Roles role); void assignRoles(Roles role); @@ -101,7 +101,7 @@ class Endpoint { std::wstring descriptionName; std::wstring deviceName; std::wstring endpointId; - unsigned long endpointState; + EndpointState endpointState; Roles endpointRoles = (Roles)0; uint64_t idx; //Not implemented in llvm-mingw. Sad! todo: mingw patch @@ -118,6 +118,7 @@ class EndpointVolumeCallback : public IAudioEndpointVolumeCallback { ULONG Release(); HRESULT QueryInterface(REFIID riid, VOID **ppvInterface); HRESULT OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA update); + void updateVolumeInfo(AUDIO_VOLUME_NOTIFICATION_DATA newVolume, float* channelVolumes); //~EndpointVolumeCallback(); private: @@ -146,15 +147,18 @@ class Overseer { public: Overseer(); + void registerEndpointSituationCallback(); NGuid getGuid(); std::vector getPlaybackEndpoints(); std::vector getCaptureEndpoints(); void updateEndpointInfo(std::wstring endpointId); - void reloadEndpoints(Flows flow); + void createEndpoints(Flows flow); Endpoint* addEndpoint(std::wstring endpointId, /* out */ Flows* flow); + std::mutex playbackMutex; + std::mutex captureMutex; //void setEndpointStatusCallback(); //void setEndpointStatusCallback(); @@ -166,8 +170,6 @@ class Overseer { ~Overseer(); private: - std::wstring exeAbsPath; - void initCOMLibrary(); NGuid guid; @@ -177,7 +179,6 @@ class Overseer { std::vector playbackDevices; std::vector captureDevices; - IPolicyConfig7* policyConfig; friend class Endpoint; //IMMDeviceCollection *deviceCollection; @@ -222,7 +223,9 @@ namespace Environment { bool isToRunAtStartup(); uint32_t getAccentColor(); + //todo: binary path cache unused static std::wstring exeAbsPath; + static uint32_t exeAbsPathLen; static bool lightMode; static bool startup = false; static HKEY scope; diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index f98369f..5816ac5 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -93,8 +93,10 @@ HRESULT SessionStateCallback::OnStateChanged(AudioSessionState NewState) { } HRESULT SessionStateCallback::OnSessionDisconnected(AudioSessionDisconnectReason DisconnectReason) { - sh->setState(SessionState::DISCONNECTED); - sh->reviseSessionShowing(SessionState::DISCONNECTED); + if (DisconnectReason != DisconnectReasonDeviceRemoval) { + sh->setState(SessionState::DISCONNECTED); + sh->reviseSessionShowing(SessionState::DISCONNECTED); + } return S_OK; } diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index ad85647..54a349c 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -24,7 +24,6 @@ void setConfigDirToDefaults() { } EndpointHandler::EndpointHandler(uint64_t idx, Flows flow) { - //std::vector endpoints = osh->getPlaybackEndpoints().at(idx); this->idx = idx; this->flow = flow; this->ep = (flow == Flows::FLOW_PLAYBACK ? osh->getPlaybackEndpoints().at(idx) : osh->getCaptureEndpoints().at(idx)); @@ -62,16 +61,6 @@ Flows EndpointHandler::getFlow(){ return ep->getFlow(); } -/* these two, currently unused. If I use them, I should feel bad. - * Endpoint* EndpointHandler::getEndpoint() { - * return this->ep; - * } - * - * EndpointVolumeCallback* EndpointHandler::getEndpointVolumeCallback() { - * return this->epc; - * } - */ - BackEndpointVolumeCallbackInfo* EndpointHandler::getCallbackInfo(){ return &this->callbackInfo; } @@ -134,12 +123,12 @@ void EndpointHandler::setBackEndpointVolumeCallbackInfoContent(uint8_t state) { } } -void EndpointHandler::setState(uint8_t state){ +void EndpointHandler::setState(EndpointState state){ ep->setState(state); this->setBackEndpointVolumeCallbackInfoContent(state); } -void EndpointHandler::setState(uint8_t state, uint64_t index){ +void EndpointHandler::setState(EndpointState state, uint64_t index){ ep->setState(state); this->setFrontVisibilityInfo((EndpointState)state, index); this->setBackEndpointVolumeCallbackInfoContent(state); @@ -318,7 +307,7 @@ uint64_t OverseerHandler::getCaptureEndpointsCount(){ return this->os->getCaptureEndpoints().size(); } -void OverseerHandler::reloadEndpointHandlers(){ +void OverseerHandler::createEndpointHandlers(){ //todo: add capture //std::vector* ephs = new std::vector; @@ -326,9 +315,7 @@ void OverseerHandler::reloadEndpointHandlers(){ for(uint64_t i = 0; i < this->getPlaybackEndpointsCount(); i++){ log_debugcpp("Creating Playback handler " + std::to_string(i)); - - EndpointHandler* ephexx = new EndpointHandler(i, Flows::FLOW_PLAYBACK); - //this->playbackEndpointHandlers.push_back(ephexx); + new EndpointHandler(i, Flows::FLOW_PLAYBACK); log_debugcpp("Created Playback handler " + std::to_string(i) + ", adding to vector. " + " VSize: " + std::to_string(this->playbackEndpointHandlers.size())); } @@ -336,37 +323,27 @@ void OverseerHandler::reloadEndpointHandlers(){ log_debugcpp("Capture VSize: " + std::to_string(this->getCaptureEndpointsCount())); - for(uint64_t i = 0; i < this->getCaptureEndpointsCount(); i++){ + for(uint64_t i = 0; i < this->getCaptureEndpointsCount(); i++) { log_debugcpp("Creating Capture handler " + std::to_string(i)); - - /* - * if(i < (this->captureEndpointHandlers.size()) && - * this->captureEndpointHandlers.at(i) != nullptr) - * delete captureEndpointHandlers.at(i); - */ - - EndpointHandler* ephoo = new EndpointHandler(i, Flows::FLOW_CAPTURE); - //this->captureEndpointHandlers.push_back(ephoo); + new EndpointHandler(i, Flows::FLOW_CAPTURE); log_debugcpp("Created Capture handler " + std::to_string(i) + ", adding to vector. " + " VSize: " + std::to_string(this->captureEndpointHandlers.size())); - /* * if (i >= this->captureEndpointHandlers.size()) * captureEndpointHandlers.push_back(eph); * else captureEndpointHandlers.at(i) = eph; */ } - //setEndpointHandlers(ephs); + os->registerEndpointSituationCallback(); } -EndpointHandler* OverseerHandler::addEndpoint(std::wstring endpointId, /* out */ Flows *flow = nullptr){ +EndpointHandler* OverseerHandler::addEndpoint(std::wstring endpointId, /* out */ Flows *flow = nullptr) { + //This method is only called from the new endpoint callback and its subsequent thread Flows localFlow; Endpoint* newEp = this->os->addEndpoint(endpointId, &localFlow); uint64_t ephIdx = (localFlow == Flows::FLOW_PLAYBACK ? this->getPlaybackEndpointsCount() : this->getCaptureEndpointsCount()) - 1; EndpointHandler* newEph = new EndpointHandler(ephIdx, localFlow); - // std::vector getPlaybackEndpointHandlers(); - //std::vector getCaptureEndpointHandlers(); if (flow != nullptr) *flow = localFlow; return newEph; } @@ -403,6 +380,10 @@ void OverseerHandler::updateFrontEndpointName(Endpoint* ep) { void OverseerHandler::reviseEndpointShowing(std::wstring endpointId, EndpointState state) { //TODO: Race condition!!!!! std::vector allHandlers; + handlersPlaybackMutex.lock(); + handlersCaptureMutex.lock(); + os->playbackMutex.lock(); + os->captureMutex.lock(); allHandlers.insert(allHandlers.end(), this->captureEndpointHandlers.begin(), this->captureEndpointHandlers.end()); allHandlers.insert(allHandlers.end(), this->playbackEndpointHandlers.begin(), this->playbackEndpointHandlers.end()); EndpointHandler* eph = nullptr; @@ -425,11 +406,15 @@ void OverseerHandler::reviseEndpointShowing(std::wstring endpointId, EndpointSta //todo: mic done but disabled. Tab-kun will come... if (flow == Flows::FLOW_CAPTURE) return; - if(eph && EndpointState::ENDPOINT_ACTIVE & state) { + if (eph && EndpointState::ENDPOINT_ACTIVE & state) { this->addEndpointWidget(eph); - } else if (eph && eph->getFrontVisibilityState() == EndpointState::ENDPOINT_ACTIVE){ + } else if (eph && eph->getFrontVisibilityState() == EndpointState::ENDPOINT_ACTIVE) { this->removeEndpointWidget(eph->getFrontVisibilityIndex()); } + handlersPlaybackMutex.unlock(); + handlersCaptureMutex.unlock(); + os->playbackMutex.unlock(); + os->captureMutex.unlock(); return; } diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index 9d43088..91d225b 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -58,8 +58,8 @@ public: void setVolume(NGuid guid, int channel, int value); void setMute(NGuid guid, bool muted); - void setState(uint8_t state); - void setState(uint8_t state, uint64_t idx); + void setState(EndpointState state); + void setState(EndpointState state, uint64_t idx); float getPeakVolume(); /* sessions */ @@ -137,10 +137,12 @@ public: void pushBackEndpointHandler(EndpointHandler* eph, Flows flow); uint64_t getPlaybackEndpointsCount(); uint64_t getCaptureEndpointsCount(); - void reloadEndpointHandlers(); + void createEndpointHandlers(); EndpointHandler* addEndpoint(std::wstring endpointId, Flows *flow); NGuid getGuid(); + std::mutex handlersPlaybackMutex; + std::mutex handlersCaptureMutex; /* * void setSessionVolumeCallback(std::function changeSessionVolume); * void setSessionVolume(float newValue, ); @@ -150,6 +152,7 @@ private: Overseer *os; std::vector playbackEndpointHandlers; std::vector captureEndpointHandlers; + std::function changeFrontDefaults; std::function removeEndpointWidget; std::function addEndpointWidget; diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 07adb81..667ce99 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -1088,7 +1088,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { mainMenuBar->addWidget(hw); mainMenuBar->setMovable(false); this->addToolBar(Qt::BottomToolBarArea, mainMenuBar); - + + /* + * Create initial endpoint widgets batch + */ reloadEndpointWidgets(); /* @@ -1319,7 +1322,8 @@ void MainWindow::trayIconActivated(QSystemTrayIcon::ActivationReason reason) { void MainWindow::reloadEndpointWidgets() { size_t i = 0; ews.resize(2); - //todo: -log flag + + osh->handlersPlaybackMutex.lock(); for (size_t epwIndex = 2; i < (osh->getPlaybackEndpointHandlers().size()); i++) { if (osh->getPlaybackEndpointHandlers().at(i)->getState() == EndpointState::ENDPOINT_ACTIVE){ log_debugcpp("EPWidget creation"); @@ -1336,6 +1340,7 @@ void MainWindow::reloadEndpointWidgets() { { ews.push_back(epw); epw->setIndex(ews.size() - 1); } } } + osh->handlersPlaybackMutex.unlock(); //todo: ya no está aquí tirao como tal, y de hecho, no ha fallado de la manera arriba descrita //pero ahora lo añado porque sí solo para garantizar que está y poder reusarlo luego lmao widgetLayout->addItem(lastRowSpacer, i, 0); diff --git a/src/qtestmain.cpp b/src/qtestmain.cpp index b036c72..d264d01 100644 --- a/src/qtestmain.cpp +++ b/src/qtestmain.cpp @@ -121,7 +121,7 @@ int main (int argc, char* argv[]) { //INIT CONT log_debugcpp("main init"); - osh->reloadEndpointHandlers(); + osh->createEndpointHandlers(); log_debugcpp("Reloaded endpoint handlers"); //INIT FRONT From f0d263d96e611aba980e23edcdbf8c7f54100eb9 Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 15 Jan 2025 21:49:41 +0100 Subject: [PATCH 30/38] changed misleading log message --- src/settings.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/settings.cpp b/src/settings.cpp index 6845254..f76e73c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -193,7 +193,11 @@ namespace ini { (create ? OPEN_ALWAYS : OPEN_EXISTING), NULL); if(settingsHandle == INVALID_HANDLE_VALUE) { + std::string createString = std::string(create ? (char*)"create" : (char*)"open"); log_debugcpp("[SET] Can't create settings file: " + std::to_string(GetLastError())); + log_debugcpp("[SET] Can't " + createString + \ + " settings file on: " + std::string(path) + \ + ", error: " + std::to_string(GetLastError())); releaseBeforeReturn(); return nullptr; } From c60b6f71ed7417ed07867c279636d80d2cec2c81 Mon Sep 17 00:00:00 2001 From: Hane Date: Wed, 15 Jan 2025 21:50:28 +0100 Subject: [PATCH 31/38] updated build script & small installer name fix --- bueno.bat | 5 ++++- install/installer.nsi | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bueno.bat b/bueno.bat index 3e2ab0d..37b5dec 100644 --- a/bueno.bat +++ b/bueno.bat @@ -1,4 +1,7 @@ -taskkill /F /IM "qtest.exe" +taskkill /F /IM "MixerQ.exe" +taskkill /F /IM "MixerQd.exe" qmake -o build\Makefile .\qtest.pro mingw32-make.exe -C .\build -f Makefile.Release mingw32-make.exe -C .\build -f Makefile.Debug +makensis /DBUILDTYPE=release install\installer.nsi +makensis /DBUILDTYPE=debug install\installer.nsi diff --git a/install/installer.nsi b/install/installer.nsi index 9181444..7e62045 100644 --- a/install/installer.nsi +++ b/install/installer.nsi @@ -1,6 +1,6 @@ ;Auto versioning------------------------------- - !makensis "/DBUILDTYPE=$BUILDTYPE version.nsi" + !makensis "/DBUILDTYPE=${BUILDTYPE} version.nsi" !system "GetVersion.exe" !include "Version.txt" ;optional cleanup From a3d00be3fe489437f064de72e7761b188345e43a Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 16 Jan 2025 18:09:02 +0100 Subject: [PATCH 32/38] fixed callback not released + unneded env func param --- src/back/backlasses.cpp | 2 +- src/back/backlasses.h | 2 +- src/cont/contclasses.cpp | 2 +- src/cont/contsessionclasses.cpp | 4 ++++ src/cont/contsessionclasses.h | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index a2271ea..7219279 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -741,7 +741,7 @@ wchar_t* Environment::getExeAbsPath(uint32_t *exeAbsPathLength) { return exeAbsPath; } -std::string Environment::createSettingsPath(SettingsTargetDirectory target, bool create) { +std::string Environment::createSettingsPath(SettingsTargetDirectory target) { wchar_t* settingsPath = nullptr; wchar_t settingsFile[] = L"\\settings.ini"; uint32_t settingsFileLen = (sizeof(settingsFile) / sizeof(wchar_t)) - 1; diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 5876b4b..4fdf888 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -211,7 +211,7 @@ class EndpointNewSessionCallback : public IAudioSessionNotification { namespace Environment { wchar_t* getExeAbsPath(uint32_t *exeAbsPathLength); - std::string createSettingsPath(SettingsTargetDirectory target, bool create); + std::string createSettingsPath(SettingsTargetDirectory target); void populateSystemValues(); void openControlPanel(); ProcessedNativeEvent processTopLevelWindowMessage(void* msg); diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 54a349c..3cfa3da 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -3,7 +3,7 @@ void setConfigDirToDefaults() { #define tryFileDir(dir, create) do { \ - OverseerHandler::settingsPath = Environment::createSettingsPath(dir, create); \ + OverseerHandler::settingsPath = Environment::createSettingsPath(dir); \ set = ini::UserSettings::createSettings(OverseerHandler::settingsPath.c_str(), create); \ if(set) { \ return; \ diff --git a/src/cont/contsessionclasses.cpp b/src/cont/contsessionclasses.cpp index fd53e62..0f6caaf 100644 --- a/src/cont/contsessionclasses.cpp +++ b/src/cont/contsessionclasses.cpp @@ -85,3 +85,7 @@ void SessionHandler::reviseSessionShowing(SessionState state) { } } +SessionHandler::~SessionHandler() { + session->removeStateCallback(ssc); + ssc->Release(); +} diff --git a/src/cont/contsessionclasses.h b/src/cont/contsessionclasses.h index 488d3ea..455bbe1 100644 --- a/src/cont/contsessionclasses.h +++ b/src/cont/contsessionclasses.h @@ -33,7 +33,7 @@ class SessionHandler { void setName(std::wstring newName); void reviseSessionShowing(SessionState state); SessionVolumeInfo* getVolumeInfo(); - + ~SessionHandler(); private: SessionVolumeInfo svi; EndpointHandler* eph; From 0a301fb9bf815ab819737ec5d85f879109ee417c Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 16 Jan 2025 20:29:02 +0100 Subject: [PATCH 33/38] fixed new endpoints not appering if UI open + no roles / deque --- src/back/backlasses.cpp | 11 +++++++++-- src/global.h | 1 + src/qt/qtclasses.cpp | 11 +++++++---- src/qt/qtclasses.h | 3 ++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 7219279..8e1a152 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -204,9 +204,16 @@ HRESULT EndpointSituationCallback::OnDefaultDeviceChanged(EDataFlow flow, ERole } std::wstring wstringEndpointId = pwstrDeviceId; log_wdebugcpp(L"we got za defol 4 " + wstringEndpointId); + //APTTYPE aptType; + //APTTYPEQUALIFIER aptQualifier; + //CoGetApartmentType( + // &aptType, + // &aptQualifier + // ); + //std::thread roleChangeThread(&OverseerHandler::roleBucketEntryCallback, this, + // nRole, wstringEndpointId); + //roleChangeThread.detach(); osh->roleBucketEntryCallback(nRole, wstringEndpointId); - //osh->changeFrontDefaultsCallback(nRole, wstringEndpointId); - return S_OK; } diff --git a/src/global.h b/src/global.h index 88f2dae..8799892 100644 --- a/src/global.h +++ b/src/global.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "debug.h" //#include "settings.h" diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 667ce99..1712d3c 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -792,7 +792,9 @@ void MainWindow::removeEndpointWidget(CustomWidgetEvent* ev){ void MainWindow::addEndpointWidget(CustomWidgetEvent* ev){ EndpointWidget* epw = new EndpointWidget(ev->payload, containerWidget, this->ews.size()); //epw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - //this->widgetLayout->addWidget(epw); + epw->setParent(this); + if(this->widgetLayout) + this->widgetLayout->addWidget(epw); ews.push_back(epw); return; } @@ -984,7 +986,9 @@ HeaderWidget::HeaderWidget(QWidget *parent) : QWidget(parent) { void MainWindow::createLayout(QGridLayout *newLayout) { log_debugcpp("createLayout"); widgetLayout->removeItem(lastRowSpacer); - delete this->widgetLayout; + QGridLayout *tempStore = this->widgetLayout; + this->widgetLayout = 0; + delete tempStore; containerWidget->setLayout(newLayout); this->widgetLayout = newLayout; @@ -1191,9 +1195,8 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event) { } void MainWindow::flushRoleChanges() { - //TODO: bucket list deque std::pair change = roleBucketList.front(); - roleBucketList.erase(roleBucketList.begin()); + roleBucketList.pop_front(); EndpointWidget *newDef = nullptr, *oldDef = nullptr; for (uint64_t i = 0; i < ews.size(); i++) { diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 6d1ec61..73a6b2c 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -208,7 +208,8 @@ private: void changeFrontDefaults(Roles role, EndpointWidget* newDef, EndpointWidget* oldDef); std::vector ews; - std::vector> roleBucketList; + std::deque> roleBucketList; + std::mutex roleBucketMutex; QWidget *containerWidget; QGridLayout *widgetLayout; From 109330dbbdcb873c3e3d21f7ecd82312dbc44cfe Mon Sep 17 00:00:00 2001 From: Hane Date: Thu, 16 Jan 2025 22:11:10 +0100 Subject: [PATCH 34/38] fixed lots of edge cases revolving role changes --- src/back/backlasses.cpp | 9 --------- src/qt/qtclasses.cpp | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 8e1a152..4b64b8e 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -204,15 +204,6 @@ HRESULT EndpointSituationCallback::OnDefaultDeviceChanged(EDataFlow flow, ERole } std::wstring wstringEndpointId = pwstrDeviceId; log_wdebugcpp(L"we got za defol 4 " + wstringEndpointId); - //APTTYPE aptType; - //APTTYPEQUALIFIER aptQualifier; - //CoGetApartmentType( - // &aptType, - // &aptQualifier - // ); - //std::thread roleChangeThread(&OverseerHandler::roleBucketEntryCallback, this, - // nRole, wstringEndpointId); - //roleChangeThread.detach(); osh->roleBucketEntryCallback(nRole, wstringEndpointId); return S_OK; } diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 1712d3c..ff5cb51 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -626,6 +626,7 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i defaultRolesCheckBoxes.at(role)->setDisabled(assignedRoles optor role ? true : false); \ defaultRolesCheckBoxes.at(role)->setText(STRING_##role); \ connect(defaultRolesCheckBoxes.at(role), &QCheckBox::stateChanged,[this] { \ + defaultRolesCheckBoxes.at(role)->setChecked(!(defaultRolesCheckBoxes.at(role)->isChecked())); \ this->eph->setRoles(role); \ }); \ widgetLayout->addWidget(defaultRolesCheckBoxes.at(role), row, col++); \ @@ -1200,6 +1201,7 @@ void MainWindow::flushRoleChanges() { EndpointWidget *newDef = nullptr, *oldDef = nullptr; for (uint64_t i = 0; i < ews.size(); i++) { + if(newDef && oldDef) break; auto epw = this->ews.at(i); if (!epw) continue; if (epw->getEndpointHandler()->getId() == change.second) { @@ -1218,20 +1220,28 @@ void MainWindow::flushRoleChanges() { } void MainWindow::changeFrontDefaults(Roles role, EndpointWidget* newDef, EndpointWidget* oldDef) { - //Sigh... I hope to get this right when librole is done... - //todo: STILL BORKED. THEY'RE NOT FORFEITING THEIR OLD POSITION - //debug if (role != Roles::ROLE_COMMUNICATIONS) return; + //Sigh... MS's naive (non)API didn't help, but... + //Since widgets are removed previously, they must be added back. + //This produces unneeded vector size increases. + //Also, there's no freaking way this must be this illegible. + //TODO: Rewrite this method. Seriously. You'll have to get to, someday. if (newDef && !oldDef) { newDef->getDefaultRolesWidgets().at(role)->blockSignals(true); - newDef->getEndpointHandler()->assignRoles(role); - QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + uint8_t newDefCurRolesReversed = ~(newDef->getEndpointHandler()->getRoles()); + if(newDefCurRolesReversed & role) { + newDef->getEndpointHandler()->assignRoles(role); + QCoreApplication::instance()->sendEvent(newDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + } uint8_t newDefIdx = newDef->getIndex(); uint8_t newDefRoles = newDef->getEndpointHandler()->getRoles(); if (newDefRoles == Roles::ROLE_ALL) { newDef->setIndex(0); this->ews[0] = newDef; - //if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; - QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + if(newDefCurRolesReversed & role) { + newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL)->blockSignals(true); + QCoreApplication::instance()->sendEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL)->blockSignals(false); + } } else if ((newDefRoles & Roles::ROLE_MULTIMEDIA) || (newDefRoles & Roles::ROLE_CONSOLE)){ newDef->setIndex(0); @@ -1246,14 +1256,15 @@ void MainWindow::changeFrontDefaults(Roles role, EndpointWidget* newDef, Endpoin else if (oldDef && newDef) { newDef->getDefaultRolesWidgets().at(role)->blockSignals(true); newDef->getEndpointHandler()->assignRoles(role); - QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + QCoreApplication::instance()->sendEvent(newDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); uint8_t newDefIdx = newDef->getIndex(); uint8_t newDefRoles = newDef->getEndpointHandler()->getRoles(); if (newDefRoles == Roles::ROLE_ALL) { newDef->setIndex(0); this->ews[0] = newDef; - //if (this->ews[1] && newDef->getEndpointHandler()->getId() == endpointId) this->ews[1] = nullptr; - QCoreApplication::instance()->postEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL)->blockSignals(true); + QCoreApplication::instance()->sendEvent(newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + newDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL)->blockSignals(false); } else if ((newDefRoles & Roles::ROLE_MULTIMEDIA) || (newDefRoles & Roles::ROLE_CONSOLE)){ newDef->setIndex(0); @@ -1267,9 +1278,13 @@ void MainWindow::changeFrontDefaults(Roles role, EndpointWidget* newDef, Endpoin oldDef->getDefaultRolesWidgets().at(role)->blockSignals(true); uint8_t oldDefRoles = oldDef->getEndpointHandler()->getRoles(); - if (oldDefRoles == Roles::ROLE_ALL) QCoreApplication::instance()->postEvent(oldDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + if (oldDefRoles == Roles::ROLE_ALL) { + oldDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL)->blockSignals(true); + QCoreApplication::instance()->sendEvent(oldDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + oldDef->getDefaultRolesWidgets().at(Roles::ROLE_ALL)->blockSignals(false); + } oldDef->getEndpointHandler()->removeRoles(role); - QCoreApplication::instance()->postEvent(oldDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); + QCoreApplication::instance()->sendEvent(oldDef->getDefaultRolesWidgets().at(role), new QEvent((QEvent::Type)CustomQEvent::EndpointDefaultChange)); oldDefRoles = oldDef->getEndpointHandler()->getRoles(); //this->ews[oldDef->getIndex()] = nullptr; if ((oldDefRoles & Roles::ROLE_MULTIMEDIA) && @@ -1291,7 +1306,7 @@ void MainWindow::changeFrontDefaults(Roles role, EndpointWidget* newDef, Endpoin log_debugcpp("oldDef new idx: " + std::to_string(oldDef->getIndex())); oldDef->getDefaultRolesWidgets().at(role)->blockSignals(false); } - QCoreApplication::instance()->postEvent + QCoreApplication::instance()->sendEvent (this, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); } From 53f9506115b2b7f1df5122b8bc07eb3013059274 Mon Sep 17 00:00:00 2001 From: Hane Date: Sat, 18 Jan 2025 18:01:35 +0100 Subject: [PATCH 35/38] fixed newly arisen race cons (busywaits) --- src/back/backlasses.cpp | 31 +++++++++++++++++++++++++++++-- src/back/backlasses.h | 10 ++++++++-- src/cont/contclasses.cpp | 20 ++++++++++++++++++-- src/cont/contclasses.h | 9 ++++----- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index 4b64b8e..cead61f 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -48,8 +48,10 @@ HRESULT EndpointNewSessionCallback::OnSessionCreated(IAudioSessionControl *NewSe Session* newSession = new Session(this->eph->getEndpoint(), sessionControl); SessionThreadParams tp = { .eph = this->eph, .session = newSession, .isDelete = false }; - std::thread newSessionThread(EndpointNewSessionCallback::createSessionThread, tp); + wait = true; + std::thread newSessionThread(&EndpointNewSessionCallback::createSessionThread, this, tp); newSessionThread.detach(); + while(wait); } return S_OK; @@ -57,6 +59,7 @@ HRESULT EndpointNewSessionCallback::OnSessionCreated(IAudioSessionControl *NewSe void EndpointNewSessionCallback::createSessionThread(SessionThreadParams params) { params.eph->addSessionSendFront(params.session); + this->wait = false; } EndpointVolumeCallback::EndpointVolumeCallback(Endpoint* ep){ @@ -103,8 +106,10 @@ HRESULT EndpointVolumeCallback::OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify for (int i = 0; i < pNotify->nChannels; i++) { channelVolumes[i] = pNotify->afChannelVolumes[i]; } + wait = true; std::thread updateVolumeThread(&EndpointVolumeCallback::updateVolumeInfo, this, paramCopy, channelVolumes); updateVolumeThread.detach(); + while(wait); return S_OK; } @@ -112,6 +117,9 @@ void EndpointVolumeCallback::updateVolumeInfo(AUDIO_VOLUME_NOTIFICATION_DATA new //delete osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller; //osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.freeData4(); //Could've made a function or = override to hide this within Nguid, but back in cont = bad. + osh->handlersPlaybackMutex.lock(); + osh->handlersCaptureMutex.lock(); + osh->lockEndpoints(); osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data1 \ = newVolume.guidEventContext.Data1; osh->getPlaybackEndpointHandlers().at(this->ep->getIndex())->getCallbackInfo()->caller.data2 \ @@ -146,9 +154,15 @@ void EndpointVolumeCallback::updateVolumeInfo(AUDIO_VOLUME_NOTIFICATION_DATA new j++; } free(channelVolumes); + osh->unlockEndpoints(); + osh->handlersPlaybackMutex.unlock(); + osh->handlersCaptureMutex.unlock(); + wait = false; } - +void EndpointVolumeCallback::reportFinished() { + this->wait = false; +} EndpointSituationCallback::EndpointSituationCallback(Overseer* os){ this->os = os; @@ -235,18 +249,26 @@ HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, D newState = EndpointState::ENDPOINT_UNPLUGGED; break; } + isEpStateChanging = true; std::thread newEndpointThread(&OverseerHandler::reviseEndpointShowing, osh, endpointId, newState); newEndpointThread.detach(); + while(isEpStateChanging); return S_OK; } HRESULT EndpointSituationCallback::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { + isEpStateChanging = true; std::thread propertyThread(&Overseer::updateEndpointInfo, os, std::wstring(pwstrDeviceId)); propertyThread.detach(); + while(isEpStateChanging); return S_OK; } +void EndpointSituationCallback::reportFinishedStateChange() { + this->isEpStateChanging = false; +} + Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx) { this->endpoint = ep; this->idx = idx; @@ -665,6 +687,10 @@ Endpoint* Overseer::addEndpoint(std::wstring endpointId, /* out */Flows* flow = return endpoint; } +void Overseer::reportFinishedStateChange() { + epsc.reportFinishedStateChange(); +} + Overseer::Overseer() : epsc(this) { log_debugcpp("Initializing Overseer"); @@ -718,6 +744,7 @@ void Overseer::updateEndpointInfo(std::wstring endpointId) { } } playbackMutex.unlock(); + epsc.reportFinishedStateChange(); } Overseer::~Overseer(){ diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 4fdf888..9f79924 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -119,11 +119,13 @@ class EndpointVolumeCallback : public IAudioEndpointVolumeCallback { HRESULT QueryInterface(REFIID riid, VOID **ppvInterface); HRESULT OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA update); void updateVolumeInfo(AUDIO_VOLUME_NOTIFICATION_DATA newVolume, float* channelVolumes); + void reportFinished(); //~EndpointVolumeCallback(); private: ULONG ref = 1; Endpoint* ep; + bool wait = false; }; class EndpointSituationCallback : public IMMNotificationClient { @@ -137,10 +139,11 @@ class EndpointSituationCallback : public IMMNotificationClient { HRESULT OnDeviceRemoved(LPCWSTR pwstrDeviceId); HRESULT OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState); HRESULT OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key); - + void reportFinishedStateChange(); private: ULONG ref = 1; Overseer* os; + bool isEpStateChanging = false; }; class Overseer { @@ -156,6 +159,8 @@ class Overseer { void createEndpoints(Flows flow); Endpoint* addEndpoint(std::wstring endpointId, /* out */ Flows* flow); + + void reportFinishedStateChange(); std::mutex playbackMutex; std::mutex captureMutex; @@ -196,9 +201,10 @@ class EndpointNewSessionCallback : public IAudioSessionNotification { ULONG Release(); HRESULT QueryInterface(REFIID riid, VOID **ppvInterface); HRESULT OnSessionCreated(IAudioSessionControl *NewSession); - static void createSessionThread(SessionThreadParams params); + void createSessionThread(SessionThreadParams params); private: + bool wait = false; ULONG ref = 1; EndpointHandler *eph; diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 3cfa3da..27ae4a6 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -348,6 +348,10 @@ EndpointHandler* OverseerHandler::addEndpoint(std::wstring endpointId, /* out */ return newEph; } +void OverseerHandler::reportFinishedStateChange() { + os->reportFinishedStateChange(); +} + NGuid OverseerHandler::getGuid() { return this->os->getGuid(); } @@ -397,24 +401,26 @@ void OverseerHandler::reviseEndpointShowing(std::wstring endpointId, EndpointSta //debug Flows flow; if (!eph) { - if (state ^ EndpointState::ENDPOINT_ACTIVE) return; + if (state ^ EndpointState::ENDPOINT_ACTIVE) goto end; //flow = Flows::FLOW_CAPTURE; eph = osh->addEndpoint(endpointId, &flow); } else flow = eph->getFlow(); //todo: mic done but disabled. Tab-kun will come... - if (flow == Flows::FLOW_CAPTURE) return; + if (flow == Flows::FLOW_CAPTURE) goto end; if (eph && EndpointState::ENDPOINT_ACTIVE & state) { this->addEndpointWidget(eph); } else if (eph && eph->getFrontVisibilityState() == EndpointState::ENDPOINT_ACTIVE) { this->removeEndpointWidget(eph->getFrontVisibilityIndex()); } + end: handlersPlaybackMutex.unlock(); handlersCaptureMutex.unlock(); os->playbackMutex.unlock(); os->captureMutex.unlock(); + os->reportFinishedStateChange(); return; } @@ -430,3 +436,13 @@ void OverseerHandler::setEndpointHandlers(std::vector ephs){ this->playbackEndpointHandlers = ephs; } +void OverseerHandler::lockEndpoints() { + os->playbackMutex.lock(); + os->captureMutex.lock(); +} + +void OverseerHandler::unlockEndpoints() { + os->playbackMutex.unlock(); + os->captureMutex.unlock(); +} + diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index 91d225b..2034986 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -138,15 +138,14 @@ public: uint64_t getPlaybackEndpointsCount(); uint64_t getCaptureEndpointsCount(); void createEndpointHandlers(); - EndpointHandler* addEndpoint(std::wstring endpointId, Flows *flow); + EndpointHandler* addEndpoint(std::wstring endpointId, Flows *flow); + void reportFinishedStateChange(); NGuid getGuid(); std::mutex handlersPlaybackMutex; std::mutex handlersCaptureMutex; - /* - * void setSessionVolumeCallback(std::function changeSessionVolume); - * void setSessionVolume(float newValue, ); - */ + void lockEndpoints(); + void unlockEndpoints(); private: Overseer *os; From f1b734bea6397153cdf742166717a4a4c54d350c Mon Sep 17 00:00:00 2001 From: Hane Date: Sat, 18 Jan 2025 18:51:32 +0100 Subject: [PATCH 36/38] fixed visuals when adding epw/sw while window is visible --- src/qt/qtclasses.cpp | 52 +++++++++++++++++++++++--------------------- src/qt/qtclasses.h | 2 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index ff5cb51..2e2a4b0 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -266,9 +266,12 @@ void MainWindow::updateColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { scrollArea->verticalScrollBar()->setPalette(pal); } -void MainWindow::compose() { - //todo: invalidate layout when adding sessions with window open +void MainWindow::compose(bool isVisible) { //We need dynamically added child widgets to expand so that we know their height + this->setUpdatesEnabled(false); + uint64_t windowWidth; + uint64_t screenHeight; + uint64_t windowHeight; /* * Setting correct widget widths and heights */ @@ -281,24 +284,22 @@ void MainWindow::compose() { screenRes.getCoords(&srx1, &sry1, &srx2, &sry2); log_debugcpp("(for Percentage) Screen Res: " + std::to_string(srx1) + " " + std::to_string(srx2)+" " + std::to_string(sry1) + " " + std::to_string(sry2)); - - uint64_t windowWidth = (((uint64_t)std::abs(screenRes.width())) * widthRatio); - uint64_t screenHeight = (uint64_t)std::abs(screenRes.height()); + windowWidth = (((uint64_t)std::abs(screenRes.width())) * widthRatio); + screenHeight = (uint64_t)std::abs(screenRes.height()); log_debugcpp("Window Width: " + std::to_string(windowWidth)); log_to_file("Window Width: %d \n", windowWidth); - - - this->createLayout(new QGridLayout()); this->scrollArea->setMaximumWidth((int)windowWidth); this->scrollArea->setMinimumWidth((int)windowWidth); this->mainMenuBar->setMinimumWidth((int)windowWidth); this->mainMenuBar->setMaximumWidth((int)windowWidth); + + this->createLayout(new QGridLayout()); for (auto *epw : ews) { if (!epw) continue; epw->updateChannelsVisibility(); epw->calculateSize(windowWidth - scrollArea->verticalScrollBar()->sizeHint().width() - - widgetLayout->contentsMargins().left() - , screenHeight); + - widgetLayout->contentsMargins().left() + , screenHeight); log_debugcpp("epw loop"); log_debugcpp("epw roles: " + print_as_binary((epw->getEndpointHandler()->getRoles()))); //std::bitset content = @@ -309,7 +310,7 @@ void MainWindow::compose() { /* * Calculating window height */ - uint64_t windowHeight = 0; + windowHeight = 0; int left = 0, top = 0, right = 0, bottom = 0; for (int i = 0; i < 2; i++) { if (!ews[i]) continue; @@ -321,13 +322,17 @@ void MainWindow::compose() { windowHeight += mainMenuBar->sizeHint().height(); log_debugcpp("windowHeight final value: " + std::to_string(windowHeight)); - //Undoing scrolling - scrollArea->verticalScrollBar()->setValue(0); - - /* - * Establishing initial window size and position - */ - setGeometry(setSizePosition(screen, windowWidth, windowHeight)); + this->setUpdatesEnabled(true); + if (!isVisible) { + /* + * Undoing scrolling + */ + scrollArea->verticalScrollBar()->setValue(0); + /* + * Establishing initial window size and position + */ + setGeometry(setSizePosition(screen, windowWidth, windowHeight)); + } } QScreen* MainWindow::getCurrentScreen() { @@ -683,7 +688,6 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i eph->getSessionHandlers().at(i)->setFrontIndex(i); } - /* Add/Remove SessionWidget callback */ eph->setAddSessionWidgetFunction([this](SessionHandler* sessionHandler) { QCoreApplication::instance()->postEvent(this, new CustomWidgetEvent((QEvent::Type)CustomQEvent::SessionWidgetCreated, sessionHandler)); @@ -707,10 +711,8 @@ void EndpointWidget::addSessionWidget(CustomWidgetEvent* ev){ const QWidgetList topLevelWidgets = QApplication::topLevelWidgets(); for (QWidget *widget : topLevelWidgets) { if (qobject_cast(widget)) { - double widthRatio = ((MainWindow*)widget)->widthRatio; - double dpr = ((MainWindow*)widget)->dpr; - sw->calculateSize((std::abs(this->screen()->geometry().width()) * widthRatio) * dpr, - std::abs(this->screen()->geometry().height())); + QCoreApplication::instance()->postEvent + (widget, new QEvent((QEvent::Type)CustomQEvent::RecomposeMainWindow)); } } this->widgetLayout->addWidget(sw, row, 0, 1, 4); @@ -768,7 +770,7 @@ void MainWindow::customEvent(QEvent* ev) { this->addEndpointWidget((CustomWidgetEvent*)ev); } else if (ev->type() == (QEvent::Type)CustomQEvent::RecomposeMainWindow) { ev->setAccepted(true); - if (this->isVisible()) this->compose(); + if (this->isVisible()) this->compose(true); } else if (ev->type() == (QEvent::Type)CustomQEvent::EndpointRoleChange) { ev->setAccepted(true); this->flushRoleChanges(); @@ -1327,7 +1329,7 @@ void MainWindow::trayIconActivated(QSystemTrayIcon::ActivationReason reason) { case QSystemTrayIcon::Trigger: if (!this->isVisible() && !recentlyClosed) { log_to_file("Recently Closed: %d \n", recentlyClosed); - this->compose(); + this->compose(false); this->showNormal(); this->activateWindow(); } diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 73a6b2c..0d5733f 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -183,7 +183,7 @@ class MainWindow : public QMainWindow { public: MainWindow(QWidget *parent = nullptr); void reloadEndpointWidgets(); - void compose(); + void compose(bool isVisible); void updateColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a); protected: From 3239e60471f71aca363293e73e74d26a1d311dfe Mon Sep 17 00:00:00 2001 From: Hane Date: Sat, 25 Jan 2025 17:27:36 +0100 Subject: [PATCH 37/38] fixed synch issues + memleak @ name fetching --- src/back/backlasses.cpp | 51 +++++++++++++++++++++++---------- src/back/backlasses.h | 15 +++++----- src/back/backsessionclasses.cpp | 15 ++++++---- src/cont/contclasses.cpp | 24 +++++++++++++--- src/cont/contclasses.h | 2 ++ src/cont/contsessionclasses.h | 8 +++--- src/qt/qtclasses.cpp | 15 +++++----- 7 files changed, 87 insertions(+), 43 deletions(-) diff --git a/src/back/backlasses.cpp b/src/back/backlasses.cpp index cead61f..74af6a0 100644 --- a/src/back/backlasses.cpp +++ b/src/back/backlasses.cpp @@ -234,6 +234,7 @@ HRESULT EndpointSituationCallback::OnDeviceRemoved(LPCWSTR pwstrDeviceId) { HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) { std::wstring endpointId = std::wstring(pwstrDeviceId); + log_wdebugcpp(L"Endpoint state change for " + endpointId); EndpointState newState; switch (dwNewState) { case DEVICE_STATE_ACTIVE: @@ -249,7 +250,7 @@ HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, D newState = EndpointState::ENDPOINT_UNPLUGGED; break; } - isEpStateChanging = true; + isEpStateChanging.exchange(true); std::thread newEndpointThread(&OverseerHandler::reviseEndpointShowing, osh, endpointId, newState); newEndpointThread.detach(); @@ -258,7 +259,7 @@ HRESULT EndpointSituationCallback::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, D } HRESULT EndpointSituationCallback::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { - isEpStateChanging = true; + isEpStateChanging.exchange(true); std::thread propertyThread(&Overseer::updateEndpointInfo, os, std::wstring(pwstrDeviceId)); propertyThread.detach(); while(isEpStateChanging); @@ -266,7 +267,8 @@ HRESULT EndpointSituationCallback::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, } void EndpointSituationCallback::reportFinishedStateChange() { - this->isEpStateChanging = false; + this->isEpStateChanging.exchange(false); + return; } Endpoint::Endpoint(IMMDevice* ep, IPolicyConfig7* policyConfig, uint64_t idx) { @@ -325,23 +327,29 @@ void Endpoint::activateEndpointSessions() { return; } - if (FAILED(endpoint->Activate(__uuidof(IAudioSessionManager2), - CLSCTX_ALL, NULL, (void**) &sessionManager))) { - log_debugcpp("Couldn't open session manager2, huh"); - return; + if (!sessionManager) { + if (FAILED(endpoint->Activate(__uuidof(IAudioSessionManager2), + CLSCTX_ALL, NULL, (void**) &sessionManager))) { + log_debugcpp("Couldn't open session manager2, huh"); + return; + } } + IAudioSessionEnumerator* sessionEnumerator = nullptr; - if (FAILED(sessionManager->GetSessionEnumerator(&sessionEnumerator))) { log_wdebugcpp(L"sesEnumeratorBros..."); return; } + if (FAILED(sessionManager->GetSessionEnumerator(&sessionEnumerator))) { log_wdebugcpp(L"sesEnumeratorBros..."); exit(-5); return; } endpointSessions.resize(1, nullptr); int sessionCount; sessionEnumerator->GetCount(&sessionCount); for (int i = 0; i < sessionCount; i++) { IAudioSessionControl* sessionControlTmp; - sessionEnumerator->GetSession(i, (IAudioSessionControl**)&sessionControlTmp); + if (FAILED(sessionEnumerator->GetSession(i, (IAudioSessionControl**)&sessionControlTmp))) { + exit(-6); + } IAudioSessionControl2* sessionControl; - sessionControlTmp->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl); - sessionControl->AddRef(); + if(FAILED(sessionControlTmp->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl))){ + exit(-7); + } sessionControlTmp->Release(); Session* session = new Session(this, sessionControl, (size_t)i); if (sessionControl->IsSystemSoundsSession() == S_OK) endpointSessions[0] = session; @@ -350,6 +358,13 @@ void Endpoint::activateEndpointSessions() { sessionEnumerator->Release(); } +/* + * void Endpoint::deleteSessionManager() { + * sessionManager->Release(); + * sessionManager = nullptr; + * } + */ + void Endpoint::addSession(Session* session) { session->setIndex(this->getSessionCount()); endpointSessions.push_back(session); @@ -357,9 +372,15 @@ void Endpoint::addSession(Session* session) { void Endpoint::activateEndpointVolume() { //If this EP is created after init, COM won't be initialized on the executing thread. - HRESULT result = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + HRESULT result = CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); if (this->endpointVolume == nullptr) { - endpoint->Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, (void**)&this->endpointVolume); + if(FAILED(endpoint->Activate( + IID_IAudioEndpointVolume, + CLSCTX_ALL, + NULL, + (void**)&this->endpointVolume))) { + log_debugcpp("No volume, huh"); + } //todo: check header if(FAILED(endpoint->Activate(__uuidof(IAudioMeterInformation), @@ -594,8 +615,8 @@ void Overseer::createEndpoints(Flows flow) { /* * Counting them */ - if(FAILED(deviceCollection->GetCount(&numEndpoints))) { log_debugcpp("si");}; - if(numEndpoints == 0) { log_debugcpp("si"); }; + if(FAILED(deviceCollection->GetCount(&numEndpoints))) { log_debugcpp("si");}; + if(numEndpoints == 0) { log_debugcpp("si"); }; /* * Retrieving actual endpoints and storing them on their own collection diff --git a/src/back/backlasses.h b/src/back/backlasses.h index 9f79924..5acedba 100644 --- a/src/back/backlasses.h +++ b/src/back/backlasses.h @@ -82,6 +82,7 @@ class Endpoint { void unregisterNewSessionNotification(EndpointNewSessionCallback* ensc); void deleteSessions(); void activateEndpointSessions(); + //void deleteSessionManager(); std::mutex endpointSessionsMutex; ~Endpoint(); @@ -91,12 +92,13 @@ class Endpoint { std::vector endpointSessions; uint32_t channelCount = 0; IMMDevice *endpoint; - IAudioClient *audioClient; + IAudioEndpointVolume *endpointVolume = nullptr; + IPropertyStore *properties; + IAudioMeterInformation *endpointPeakMeter = nullptr; + //IAudioClient *audioClient; int64_t defTime, minTime; IAudioSessionManager2 *sessionManager = nullptr; Flows flow; - IAudioEndpointVolume *endpointVolume = nullptr; - IPropertyStore *properties; std::wstring friendlyName; std::wstring descriptionName; std::wstring deviceName; @@ -105,7 +107,6 @@ class Endpoint { Roles endpointRoles = (Roles)0; uint64_t idx; //Not implemented in llvm-mingw. Sad! todo: mingw patch - IAudioMeterInformation *endpointPeakMeter = nullptr; IPolicyConfig7* policyConfig; }; @@ -125,7 +126,7 @@ class EndpointVolumeCallback : public IAudioEndpointVolumeCallback { private: ULONG ref = 1; Endpoint* ep; - bool wait = false; + std::atomic wait = false; }; class EndpointSituationCallback : public IMMNotificationClient { @@ -143,7 +144,7 @@ class EndpointSituationCallback : public IMMNotificationClient { private: ULONG ref = 1; Overseer* os; - bool isEpStateChanging = false; + std::atomic isEpStateChanging = false; }; class Overseer { @@ -204,7 +205,7 @@ class EndpointNewSessionCallback : public IAudioSessionNotification { void createSessionThread(SessionThreadParams params); private: - bool wait = false; + std::atomic wait = false; ULONG ref = 1; EndpointHandler *eph; diff --git a/src/back/backsessionclasses.cpp b/src/back/backsessionclasses.cpp index 5816ac5..c9aaa85 100644 --- a/src/back/backsessionclasses.cpp +++ b/src/back/backsessionclasses.cpp @@ -39,6 +39,7 @@ HRESULT SessionStateCallback::QueryInterface(REFIID riid, VOID **ppvInterface) { HRESULT SessionStateCallback::OnDisplayNameChanged(LPCWSTR NewDisplayName, LPCGUID EventContext) { //TODO: Preguntar while(sh->getVolumeInfo()->isNameChanged == true); + sh->setName(std::wstring(NewDisplayName)); sh->getVolumeInfo()->isNameChanged = true; return S_OK; @@ -238,13 +239,15 @@ bool Session::fetchNameViaFD(std::wstring exePath, DWORD pid, std::wstring *sess void* fileVersionInfo = malloc(fileVersionInfoSize); if(!GetFileVersionInfoExW(FILE_VER_GET_LOCALISED | FILE_VER_GET_NEUTRAL, - exePath.c_str(),0,fileVersionInfoSize, fileVersionInfo)) + exePath.c_str(),0,fileVersionInfoSize, fileVersionInfo)) { return false; + } UINT translationArrayLen = 0; - if (!VerQueryValueW(fileVersionInfo, L"\\VarFileInfo\\Translation", (LPVOID*)&translationArray, &translationArrayLen)) + if (!VerQueryValueW(fileVersionInfo, L"\\VarFileInfo\\Translation", (LPVOID*)&translationArray, &translationArrayLen)) { + free(fileVersionInfo); return false; - + } //File descriptor parsing //TODO: https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserpreferreduilanguages /* It is possible to retrieve user languages and try to use one of those before falling back to whatever @@ -257,7 +260,6 @@ bool Session::fetchNameViaFD(std::wstring exePath, DWORD pid, std::wstring *sess int8_t syslangIdx = -1; wchar_t metadataStringKey[256]; wchar_t* metadataString = NULL; - //std::wstring name; for (UINT i = 0; i < availableLangs; i++) { LANGID defaultUILanguage = GetUserDefaultUILanguage(); if (defaultUILanguage != translationArray[i].wLanguage) @@ -340,8 +342,6 @@ bool Session::fetchNameViaMSIX(std::wstring exePath, DWORD pid, std::wstring *se if (sessionName->length() > 0) return true; else return false; - - } @@ -419,4 +419,7 @@ Session::~Session() { meterInformation->Release(); sessionControl->Release(); sessionVolume->Release(); + meterInformation = nullptr; + sessionControl = nullptr; + sessionVolume = nullptr; } diff --git a/src/cont/contclasses.cpp b/src/cont/contclasses.cpp index 27ae4a6..d2188f0 100644 --- a/src/cont/contclasses.cpp +++ b/src/cont/contclasses.cpp @@ -28,7 +28,6 @@ EndpointHandler::EndpointHandler(uint64_t idx, Flows flow) { this->flow = flow; this->ep = (flow == Flows::FLOW_PLAYBACK ? osh->getPlaybackEndpoints().at(idx) : osh->getCaptureEndpoints().at(idx)); - epc = new EndpointVolumeCallback(ep); this->callbackInfo.caller = osh->getGuid(); //epName = ep->getName(); this->setBackEndpointVolumeCallbackInfoContent(this->getState()); @@ -115,7 +114,10 @@ void EndpointHandler::setBackEndpointVolumeCallbackInfoContent(uint8_t state) { callbackInfo.muted = this->getMute(); callbackInfo.mainVolume = this->getVolume(AudioChannel::CHANNEL_MAIN); callbackInfo.channels = this->getChannelCount(); - ep->setVolumeCallback(epc); + if (!epc) { + epc = new EndpointVolumeCallback(ep); + ep->setVolumeCallback(epc); + } callbackInfo.channelVolumes.resize(this->callbackInfo.channels); for(uint32_t i = 0; i < this->getChannelCount(); i++){ callbackInfo.channelVolumes.at(i) = this->getVolume(i); @@ -181,13 +183,13 @@ Endpoint* EndpointHandler::getEndpoint() { void EndpointHandler::addSessionSendFront(Session* session) { if(!ep->endpointSessionsMutex.try_lock()) return; + sessionHandlersMutex.lock(); this->ep->addSession(session); SessionHandler* sessionHandler = new SessionHandler(this, session, (getSessionCount() - 1)); - ep->endpointSessionsMutex.unlock(); - sessionHandlersMutex.lock(); sessionHandlers.push_back(sessionHandler); + ep->endpointSessionsMutex.unlock(); sessionHandlersMutex.unlock(); this->addSessionWidget(sessionHandler); } @@ -203,11 +205,13 @@ void EndpointHandler::removeSessionFromFront(SessionHandler* sh) { void EndpointHandler::deleteSessions() { ep->unregisterNewSessionNotification(ensc); ensc->Release(); + ensc = nullptr; for (auto sh : sessionHandlers) { delete sh; } sessionHandlers.resize(0); ep->deleteSessions(); + //ep->deleteSessionManager(); } void EndpointHandler::createSessionHandlers() { @@ -218,6 +222,9 @@ void EndpointHandler::createSessionHandlers() { sessionHandlers.push_back(sessionHandler); } } +} + +void EndpointHandler::createSessionHandlersCallback() { ensc = new EndpointNewSessionCallback(this); ep->registerNewSessionNotification(ensc); } @@ -232,6 +239,12 @@ void EndpointHandler::unlockSessionCollections() { this->ep->endpointSessionsMutex.unlock(); } +void EndpointHandler::removeVolumeCallback() { + ep->removeVolumeCallback(epc); + epc->Release(); + epc = nullptr; +} + EndpointHandler::~EndpointHandler() { ep->removeVolumeCallback(epc); ep->unregisterNewSessionNotification(ensc); @@ -411,10 +424,13 @@ void OverseerHandler::reviseEndpointShowing(std::wstring endpointId, EndpointSta if (flow == Flows::FLOW_CAPTURE) goto end; if (eph && EndpointState::ENDPOINT_ACTIVE & state) { + eph->setState(EndpointState::ENDPOINT_ACTIVE); this->addEndpointWidget(eph); } else if (eph && eph->getFrontVisibilityState() == EndpointState::ENDPOINT_ACTIVE) { + eph->removeVolumeCallback(); this->removeEndpointWidget(eph->getFrontVisibilityIndex()); } + end: handlersPlaybackMutex.unlock(); handlersCaptureMutex.unlock(); diff --git a/src/cont/contclasses.h b/src/cont/contclasses.h index 2034986..cd0e03e 100644 --- a/src/cont/contclasses.h +++ b/src/cont/contclasses.h @@ -76,9 +76,11 @@ public: void removeSessionFromFront(SessionHandler* sh); void deleteSessions(); void createSessionHandlers(); + void createSessionHandlersCallback(); std::mutex sessionHandlersMutex; void lockSessionCollections(); void unlockSessionCollections(); + void removeVolumeCallback(); ~EndpointHandler(); private: diff --git a/src/cont/contsessionclasses.h b/src/cont/contsessionclasses.h index 455bbe1..24c0ba7 100644 --- a/src/cont/contsessionclasses.h +++ b/src/cont/contsessionclasses.h @@ -9,10 +9,10 @@ class SessionStateCallback; struct SessionVolumeInfo { //SessionVolumeInfo(bool muted, float mainVolume); - bool muted; - float mainVolume; - NGuid caller; - bool isNameChanged = false; + bool muted; + float mainVolume; + NGuid caller; + std::atomic isNameChanged = false; //size_t channels; //std::vector channelVolumes; }; diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index 2e2a4b0..a5c391d 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -411,7 +411,8 @@ SessionWidget::SessionWidget(uint64_t idx, SessionHandler* sh, QWidget *parent) if (sh->getVolumeInfo()->isNameChanged) { mainLabel->setText(QString::fromStdWString(sh->getName())); mainLabel->setToolTip(QString::fromStdWString(sh->getName())); - } + sh->getVolumeInfo()->isNameChanged = false; + } const float roundingFactor = 0.005; mainSlider->blockSignals(true); muteButton->blockSignals(true); @@ -538,7 +539,7 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i eph->createSessionHandlers(); //todo: sussy - this->eph->setState(EndpointState::ENDPOINT_ACTIVE, idx); + this->eph->setFrontVisibilityInfo(EndpointState::ENDPOINT_ACTIVE, idx); this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); widgetLayout = new QGridLayout(this); //this->setContentsMargins(0, 0, 0, 0); @@ -698,9 +699,9 @@ EndpointWidget::EndpointWidget(EndpointHandler* eph, QWidget *parent, uint64_t i }); log_debugcpp("ENDPOINT_WIDGETED"); + eph->createSessionHandlersCallback(); } - void EndpointWidget::addSessionWidget(CustomWidgetEvent* ev){ this->setUpdatesEnabled(false); uint64_t index = this->sessionWidgets.size(); @@ -779,7 +780,7 @@ void MainWindow::customEvent(QEvent* ev) { } //__attribute__((optimize("O0", "unroll-loops"))) -void MainWindow::removeEndpointWidget(CustomWidgetEvent* ev){ +void MainWindow::removeEndpointWidget(CustomWidgetEvent* ev) { uint64_t i = ev->payload; this->ews.at(i)->setParent(nullptr); this->widgetLayout->removeWidget(ews.at(i)); @@ -792,7 +793,7 @@ void MainWindow::removeEndpointWidget(CustomWidgetEvent* ev){ return; } -void MainWindow::addEndpointWidget(CustomWidgetEvent* ev){ +void MainWindow::addEndpointWidget(CustomWidgetEvent* ev) { EndpointWidget* epw = new EndpointWidget(ev->payload, containerWidget, this->ews.size()); //epw->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); epw->setParent(this); @@ -924,9 +925,9 @@ EndpointHandler* EndpointWidget::getEndpointHandler(){ * } */ -void EndpointWidget::setIndex(uint64_t idx){ +void EndpointWidget::setIndex(uint64_t idx) { this->idx = idx; - this->eph->setState(EndpointState::ENDPOINT_ACTIVE, this->idx); + this->eph->setFrontVisibilityInfo(EndpointState::ENDPOINT_ACTIVE, this->idx); } uint64_t EndpointWidget::getIndex(){ From 8f864f33941dcfcb835a97771f5e7f803cf0141b Mon Sep 17 00:00:00 2001 From: Hane Date: Sat, 31 Jan 2026 18:09:02 +0100 Subject: [PATCH 38/38] wip: icons --- assets.qrc | 2 ++ bueno.bat | 6 +++--- qtest.pro | 2 +- src/qt/qtclasses.cpp | 38 ++++++++++++++++++++++++++++++--- src/qt/qtclasses.h | 7 +++++- src/qt/qtcommon.h | 32 +++++++++++++++++++++++++++ src/qt/qtvisuals.h | 51 ++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 128 insertions(+), 10 deletions(-) diff --git a/assets.qrc b/assets.qrc index 4ef5956..2206a8a 100644 --- a/assets.qrc +++ b/assets.qrc @@ -4,5 +4,7 @@ assets/notificationAreaIcon.png assets/style.qss assets/logo.ico + assets/mute.svg + assets/unmute.svg diff --git a/bueno.bat b/bueno.bat index 37b5dec..540be60 100644 --- a/bueno.bat +++ b/bueno.bat @@ -1,7 +1,7 @@ taskkill /F /IM "MixerQ.exe" taskkill /F /IM "MixerQd.exe" qmake -o build\Makefile .\qtest.pro -mingw32-make.exe -C .\build -f Makefile.Release +REM mingw32-make.exe -C .\build -f Makefile.Release mingw32-make.exe -C .\build -f Makefile.Debug -makensis /DBUILDTYPE=release install\installer.nsi -makensis /DBUILDTYPE=debug install\installer.nsi +REM makensis /DBUILDTYPE=release install\installer.nsi +REM makensis /DBUILDTYPE=debug install\installer.nsi diff --git a/qtest.pro b/qtest.pro index 0568bca..42a1165 100644 --- a/qtest.pro +++ b/qtest.pro @@ -18,7 +18,7 @@ LIBS += -lWinmm -lodbc32 -lodbccp32 -luuid -loleaut32 -lole32 -lshell32 -ladvapi DEFINES += QT_LOGGING_TO_CONSOLE=1 WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 DEFINES_DEBUG += DEBUG -QT += widgets network +QT += widgets network svg INCLUDEPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" VPATH += "$$PWD\src" "$$PWD\src\qt" "$$PWD\src\back" "$$PWD\src\back\reimpl" "$$PWD\src\cont" diff --git a/src/qt/qtclasses.cpp b/src/qt/qtclasses.cpp index a5c391d..a9148b9 100644 --- a/src/qt/qtclasses.cpp +++ b/src/qt/qtclasses.cpp @@ -207,6 +207,36 @@ void ExtendedCheckBox::customEvent(QEvent* ev) { QCheckBox::customEvent(ev); } +void ExtendedCheckBox::paintEvent(QPaintEvent *event) { + QStylePainter p(this); + QStyleOptionButton opt; + initStyleOption(&opt); + opt.icon = this->icons; + p.drawControl((QStyle::ControlElement)CustomControlElement::CE_ExtendedCheckBox, opt); + //QStyle* style = QApplication::style(); + //style->drawComplexControl((QStyle::ComplexControl)CC_MeterSlider, &sliderComplex2, &painter, this); +} + +void ExtendedCheckBox::addIcon(char* const path, QIcon::State state) { + QString str(path); + QSvgRenderer rr(str); + QPixmap pixmap(64, 64); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + rr.render(&painter); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + uint8_t a, r, g, b; + if (StylingHelper::argbToDiscreteValues(osh->getAccentColor(), &r, &g, &b, &a)) { + QColor color(r, g, b, a); + painter.fillRect(pixmap.rect(), color); + } + painter.end(); + icons.addPixmap(pixmap, QIcon::Normal, state); + //this->setIcon(icons); + //icons.addFile(":/Icons/images/second.svg",QSize(32,32),QIcon::Normal,QIcon::Off); +} + + QRect MainWindow::setSizePosition(QScreen* screen, int width, int height) { //setGeometry ignores decoration size, theres others for that QRect trayIconPos = this->trayIcon->geometry(); @@ -276,7 +306,7 @@ void MainWindow::compose(bool isVisible) { * Setting correct widget widths and heights */ log_to_file("[Compose]\n"); - screen = this->getCurrentScreen(); + screen = StylingHelper::getCurrentScreen(); log_debugcpp("Screen: " + screen->model().toStdString() + " " + screen->name().toStdString()); QRect screenRes = screen->geometry(); @@ -336,7 +366,7 @@ void MainWindow::compose(bool isVisible) { } QScreen* MainWindow::getCurrentScreen() { - //todo: Using cursor pos as screen detector. Flawed. + //note: Using cursor pos as screen detector. Flawed. QPoint cursorPos = QCursor::pos(); log_debugcpp("Cursor pos: " + std::to_string(cursorPos.ry()) + " " + std::to_string(cursorPos.rx())); @@ -363,7 +393,9 @@ SessionWidget::SessionWidget(uint64_t idx, SessionHandler* sh, QWidget *parent) widgetLayout->getContentsMargins(&left, &top, &right, &bottom); widgetLayout->setContentsMargins(0, top, 0, bottom); - muteButton = new QCheckBox(this); + muteButton = new ExtendedCheckBox(this); + muteButton->addIcon(":/assets/mute.svg", QIcon::On); + muteButton->addIcon(":/assets/unmute.svg", QIcon::Off); mainLabel = new QLabel(QString::fromStdWString(sh->getName()), this); mainLabel->setToolTip(QString::fromStdWString(sh->getName())); mainSlider = new MeterSlider(Qt::Horizontal, this); diff --git a/src/qt/qtclasses.h b/src/qt/qtclasses.h index 0d5733f..1b2658b 100644 --- a/src/qt/qtclasses.h +++ b/src/qt/qtclasses.h @@ -40,12 +40,17 @@ public: class ExtendedCheckBox : public QCheckBox { Q_OBJECT +private: + QIcon icons; + protected: void customEvent(QEvent* ev) override; + void paintEvent(QPaintEvent* event) override; public: //c++11: this inherits all parent's constructors unconditionally using QCheckBox::QCheckBox; + void addIcon(char* const path, QIcon::State state); //alternative being calling parent ctor directly after declaring child ctor: //B(int x) : A(x) { } }; @@ -68,7 +73,7 @@ private: MeterSlider *mainSlider = nullptr; uint64_t idx; QHBoxLayout *widgetLayout = nullptr; - QCheckBox *muteButton = nullptr; + ExtendedCheckBox *muteButton = nullptr; SessionHandler* sh; QTimer* volumePoller = nullptr; QSpacerItem* widthSpacer; diff --git a/src/qt/qtcommon.h b/src/qt/qtcommon.h index b059504..61873eb 100644 --- a/src/qt/qtcommon.h +++ b/src/qt/qtcommon.h @@ -44,6 +44,7 @@ #include #include #include +#include //#include //#include /* @@ -63,6 +64,10 @@ enum CustomComplexControl { CC_MeterSlider = 0xf0000001 }; +enum CustomControlElement { + CE_ExtendedCheckBox = 0xf0000001 +}; + namespace StylingHelper { static inline void setBackgroundColor(bool lightMode) { @@ -218,5 +223,32 @@ namespace StylingHelper { return pal.color(QPalette::Base); } + static inline QPixmap svg2Pixmap(const QString& svgContent, + const QSize& size, + QPainter::CompositionMode mode = QPainter::CompositionMode_SourceOver) + { + QSvgRenderer rr(svgContent); + QImage image(size.width(), size.height(), QImage::Format_ARGB32); + QPainter painter(&image); + painter.setCompositionMode(mode); + image.fill(Qt::transparent); + rr.render(&painter); + return QPixmap::fromImage(image); + } + + static inline QScreen* getCurrentScreen() { + //note: Using cursor pos as screen detector. Flawed. + QPoint cursorPos = QCursor::pos(); + + for (QScreen *screen : QGuiApplication::screens()) { + QRect screenRect = screen->geometry(); + if (screenRect.contains(cursorPos)) { + return screen; + } + } + + return QGuiApplication::primaryScreen(); + } + } diff --git a/src/qt/qtvisuals.h b/src/qt/qtvisuals.h index 0db58e8..8902ed1 100644 --- a/src/qt/qtvisuals.h +++ b/src/qt/qtvisuals.h @@ -20,8 +20,10 @@ public: return baseStyle()->styleHint(hint, option, widget, returnData); } - QRect subControlRect(ComplexControl control, const QStyleOptionComplex *option, - SubControl subControl, const QWidget *widget) const { + QRect subControlRect(ComplexControl control, + const QStyleOptionComplex *option, + SubControl subControl, + const QWidget *widget) const { QRect rect = QCommonStyle::subControlRect(CC_Slider, option, subControl, widget); switch (control) { @@ -186,6 +188,51 @@ public: } + void drawControl(ControlElement element, const QStyleOption *opt, + QPainter *p, const QWidget *widget) const + { + switch(element) { + case CE_ExtendedCheckBox: + if (const QStyleOptionButton *btn = qstyleoption_cast(opt)) { + QStyleOptionButton subopt = *btn; + subopt.rect = subElementRect(SE_CheckBoxIndicator, btn, widget); + //proxy()->drawPrimitive(PE_IndicatorCheckBox, &subopt, p, widget); + subopt.rect = subElementRect(SE_CheckBoxContents, btn, widget); + + //proxy()->drawControl(CE_CheckBoxLabel, &subopt, p, widget); + int alignment = visualAlignment(btn->direction, Qt::AlignLeft | Qt::AlignVCenter); + + if (!proxy()->styleHint(SH_UnderlineShortcut, btn, widget)) + alignment |= Qt::TextHideMnemonic; + QPixmap pix; + QRect textRect = btn->rect; + if (!btn->icon.isNull()) { + pix = btn->icon.pixmap(btn->iconSize, StylingHelper::getCurrentScreen()->devicePixelRatio(), + QIcon::Mode::Normal, btn->state & State_On ? QIcon::On : QIcon::Off); + proxy()->drawItemPixmap(p, btn->rect, alignment, pix); + if (btn->direction == Qt::RightToLeft) + textRect.setRight(textRect.right() - btn->iconSize.width() - 4); + else + textRect.setLeft(textRect.left() + btn->iconSize.width() + 4); + } + if (!btn->text.isEmpty()){ + proxy()->drawItemText(p, textRect, alignment | Qt::TextShowMnemonic, + btn->palette, btn->state & State_Enabled, btn->text, QPalette::WindowText); + } + // + if (btn->state & State_HasFocus) { + QStyleOptionFocusRect fropt; + fropt.QStyleOption::operator=(*btn); + fropt.rect = subElementRect(SE_CheckBoxFocusRect, btn, widget); + proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, p, widget); + } + } + default: + baseStyle()->drawControl(element, opt, p, widget); + break; + } + } + void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, QPainter *painter, const QWidget *widget) const { QColor outline;