diff --git a/qt5/GNUfiles.mk b/qt5/GNUfiles.mk new file mode 100644 index 00000000..fc4f1d1f --- /dev/null +++ b/qt5/GNUfiles.mk @@ -0,0 +1,74 @@ +# 28 may 2016 + +CXXFILES += \ + qt5/alloc.cpp \ + qt5/area.cpp \ + qt5/box.cpp \ + qt5/button.cpp \ + qt5/checkbox.cpp \ + +# qt5/child.cpp \ + +CXXFILES += \ + qt5/colorbutton.cpp \ + qt5/combobox.cpp \ + qt5/control.cpp \ + qt5/datetimepicker.cpp \ + qt5/debug.cpp \ + qt5/draw.cpp \ + qt5/drawmatrix.cpp \ + qt5/drawpath.cpp \ + qt5/drawtext.cpp \ + qt5/editablecombo.cpp \ + qt5/entry.cpp \ + qt5/fontbutton.cpp \ + qt5/group.cpp \ + qt5/label.cpp \ + qt5/main.cpp \ + qt5/menu.cpp \ + qt5/multilineentry.cpp \ + qt5/progressbar.cpp \ + qt5/radiobuttons.cpp \ + qt5/separator.cpp \ + qt5/slider.cpp \ + qt5/spinbox.cpp \ + qt5/stddialogs.cpp \ + qt5/tab.cpp \ + qt5/text.cpp \ + +# qt5/util.cpp \ + +CXXFILES += \ + qt5/window.cpp + +HFILES += \ + qt5/draw.hpp \ + qt5/uipriv_qt5.hpp + +# TODO split into a separate file or put in GNUmakefile.libui somehow? + +# flags for Qt5 +CFLAGS += $(NATIVE_UI_CFLAGS) +CXXFLAGS += $(NATIVE_UI_CXXFLAGS) +LDFLAGS += $(NATIVE_UI_LDFLAGS) + + +ifneq ($(RELEASE),1) + CXXFLAGS += -DQT_DEBUG +else + # disable Q_ASSERT checks + CXXFLAGS += -DQT_NO_DEBUG +endif + +# flags for building a shared library +# OS X does support -shared but it has a preferred name for this so let's use that there instead; hence this is not gcc-global +LDFLAGS += \ + -shared + +# flags for warning on undefined symbols +# this is not gcc-global because OS X doesn't support these flags +# TODO figure out why FreeBSD follows linked libraries here +ifneq ($(shell uname -s),FreeBSD) +LDFLAGS += \ + -Wl,--no-undefined -Wl,--no-allow-shlib-undefined +endif diff --git a/qt5/GNUinstall.mk b/qt5/GNUinstall.mk new file mode 100644 index 00000000..634dbc06 --- /dev/null +++ b/qt5/GNUinstall.mk @@ -0,0 +1,8 @@ +ifndef PREFIX + PREFIX=/usr +endif + +install: $(OUT) + cp $(OUT) $(DESTDIR)$(PREFIX)/lib/libui.so.0 + ln -fs libui.so.0 $(DESTDIR)$(PREFIX)/lib/libui.so + cp ui.h ui_$(OS).h $(DESTDIR)$(PREFIX)/include/ diff --git a/qt5/GNUosspecific.mk b/qt5/GNUosspecific.mk new file mode 100644 index 00000000..3787016b --- /dev/null +++ b/qt5/GNUosspecific.mk @@ -0,0 +1,21 @@ +# 16 october 2015 + +EXESUFFIX = +LIBSUFFIX = .so +OSHSUFFIX = .h +STATICLIBSUFFIX = .a +TOOLCHAIN = gcc + +# LONGTERM clean up all the NAMEs and SUFFIXs and NOSOSUFFIXs or whatever it was +USESSONAME = 1 +SOVERSION = $(SOVERSION0) +SONAMEEXT = $(LIBSUFFIX).$(SOVERSION) +# this is not gcc-global because OS X uses a different filename format +SONAMEFLAG = -Wl,-soname, + +NATIVE_UI_CFLAGS += \ + `pkg-config --cflags Qt5Widgets` +NATIVE_UI_CXXFLAGS += \ + `pkg-config --cflags Qt5Widgets` +NATIVE_UI_LDFLAGS += \ + `pkg-config --libs Qt5Widgets` -lm diff --git a/qt5/GNUosspecificlink.mk b/qt5/GNUosspecificlink.mk new file mode 100644 index 00000000..573b289f --- /dev/null +++ b/qt5/GNUosspecificlink.mk @@ -0,0 +1,15 @@ +# 28 may 2016 + +$(OUT): $(OFILES) | $(OUTDIR) +ifeq (,$(STATICLIB)) + @$(reallinker) -o $(OUT) $(OFILES) $(LDFLAGS) +ifeq ($(USESSONAME),1) + @ln -sf $(NAME)$(SUFFIX) $(OUTNOSONAME) +endif +else + $(LD) -r $(OFILES) -o $(OUT:%.a=%.o) + objcopy --localize-hidden $(OUT:%.a=%.o) + $(AR) rcs $(OUT) $(OUT:%.a=%.o) +endif + @echo ====== Linked $(OUT) + diff --git a/qt5/alloc.cpp b/qt5/alloc.cpp new file mode 100644 index 00000000..f92741bd --- /dev/null +++ b/qt5/alloc.cpp @@ -0,0 +1,76 @@ + +#include "uipriv_qt5.hpp" + +#include +#include +#include + +#include "stdlib.h" + +static QHash allocations; + +#ifdef QT_DEBUG +// not going to put a lock on this, just simple sanity check since the rest of the code doesn't do much about thread safety +static QThread *uiThread = nullptr; +#endif + +void initAlloc(void) +{ +#ifdef QT_DEBUG + uiThread = QThread::currentThread(); +#endif +} + +static void checkThread(const char *func) +{ +#ifdef QT_DEBUG + if (QThread::currentThread() != uiThread) { + qWarning("Invoking %s on different thread from the one used for initialization, corruption will occur!", func); + } +#endif +} + +void uninitAlloc(void) +{ + checkThread(QT_MESSAGELOG_FUNC); + if (!allocations.isEmpty()) { + qWarning("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:"); + for (auto it = allocations.constBegin(), e = allocations.constEnd(); it != e; ++it) { + auto widget = uiValidateAndCastObjTo((uiControl*)it.key()); + qWarning() << it.key() << ':' << it.value() << ':' << widget; + } + } +} + +void *uiAlloc(size_t size, const char *type) +{ + checkThread(QT_MESSAGELOG_FUNC); + auto p = malloc(size); + allocations[p] = type; + return p; +} + +void *uiRealloc(void *p, size_t new_size, const char *type) +{ + checkThread(QT_MESSAGELOG_FUNC); + + if (!allocations.remove(p)) { + qFatal("Reallocing of invalid/non-existent allocation: %p type: %s", p, type); + return nullptr; + } + + p = realloc(p, new_size); + allocations[p] = type; + return p; +} + +void uiFree(void *p) +{ + checkThread(QT_MESSAGELOG_FUNC); + if (!allocations.remove(p)) { + qWarning("Ignoring freeing of invalid/non-existent allocation: %p", p); + return; + } + + free(p); +} diff --git a/qt5/area.cpp b/qt5/area.cpp new file mode 100644 index 00000000..3a88b625 --- /dev/null +++ b/qt5/area.cpp @@ -0,0 +1,176 @@ + +#include "uipriv_qt5.hpp" + +#include "draw.hpp" + +#include +#include + +#include // TODO: remove + +struct uiArea : public uiQt5Control {}; + +class Area : public QWidget +{ + Q_DISABLE_COPY(Area) + +public: + Area(uiAreaHandler *ah) + : ah_(ah) + { + setAttribute(Qt::WA_Hover); + } + +private: + void paintEvent(QPaintEvent *event) + { + uiDrawContext painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + const auto clip = event->rect(); + + // avoid using C99 initializer for portability *cough*MSVC*cough* + uiAreaDrawParams params = { + &painter, // Context + (double)width(), // AreaWidth + (double)height(), // AreaHeight + (double)clip.x(), // ClipX; + (double)clip.y(), // ClipY; + (double)clip.width(), // ClipWidth; + (double)clip.height(), // ClipHeight; + }; + + ah_->Draw(ah_,static_cast(uiFindQt5ControlForQObject(this)),¶ms); + } + + bool event(QEvent *event) + { + switch(event->type()) { + //case QEvent::HoverMove: // don't care + case QEvent::HoverEnter: + case QEvent::HoverLeave: + translateHoverEvent(static_cast(event)); + break; + } + return QWidget::event(event); + } + + void translateHoverEvent(QHoverEvent *hoverEvent) + { + auto area = static_cast(uiFindQt5ControlForQObject(this)); + switch(hoverEvent->type()) { + case QEvent::HoverEnter: + ah_->MouseCrossed(ah_,area,0); + setFocus(Qt::MouseFocusReason); // so we get key events, hacky? + setMouseTracking(true); + break; + case QEvent::HoverLeave: + ah_->MouseCrossed(ah_,area,1); + clearFocus(); + setMouseTracking(false); + break; + } + } + + void mousePressEvent(QMouseEvent *mouseEvent) { translateMouseEvent(mouseEvent); } + void mouseReleaseEvent(QMouseEvent *mouseEvent) { translateMouseEvent(mouseEvent); } + void mouseDoubleClickEvent(QMouseEvent *mouseEvent) { translateMouseEvent(mouseEvent); } + void mouseMoveEvent(QMouseEvent *mouseEvent) { translateMouseEvent(mouseEvent); } + + void translateMouseEvent(QMouseEvent *mouseEvent) + { + static bool warned = false; + if (warned) { + qWarning("TODO: mouseEvent translation is incomplete!"); + warned = true; + } + uiAreaMouseEvent me; + auto pos = mouseEvent->pos(); + + me.X = pos.x(); + me.Y = pos.y(); + + me.AreaWidth = width(); + me.AreaHeight = height(); + + // TODO: at a glance these are not tested nor well defined..? + me.Down = 0; // TODO + me.Up = 0; // TODO + me.Count = 0; // TODO + me.Modifiers = 0; // TODO + me.Held1To64 = 0; // TODO + + ah_->MouseEvent(ah_,static_cast(uiFindQt5ControlForQObject(this)),&me); + } + + void keyPressEvent(QKeyEvent *keyEvent) { translateKeyEvent(keyEvent); } + void keyReleaseEvent(QKeyEvent *keyEvent) { translateKeyEvent(keyEvent); } + + void translateKeyEvent(QKeyEvent *keyEvent) + { + uiAreaKeyEvent ke; + + qWarning() << "TODO: keyEvent translation is incomplete!" << keyEvent; + + ke.Key = keyEvent->key(); + ke.ExtKey = 0; + ke.Modifier = 0; + ke.Modifiers = 0; + ke.Up = (keyEvent->type() == QEvent::KeyRelease); + + ah_->KeyEvent(ah_,static_cast(uiFindQt5ControlForQObject(this)),&ke); + } + +private: + uiAreaHandler *ah_ = nullptr; +}; + +static Area *findArea(uiArea *a) +{ + if (auto widget = uiValidateAndCastObjTo(a)) { + if (auto area = dynamic_cast(widget)) { + return area; + } + return widget->findChild(); + } + return nullptr; +} + +void uiAreaSetSize(uiArea *a, intmax_t width, intmax_t height) +{ + qWarning("TODO: %p %d, %d", (void *)a, (int)width, (int)height); +} + +void uiAreaQueueRedrawAll(uiArea *a) +{ + if (auto area = findArea(a)) { + area->update();; + } +} + +void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height) +{ + qWarning("TODO: %p %f, %f, %f, %f", (void *)a, x, y, width, height); +} + +uiArea *uiNewArea(uiAreaHandler *ah) +{ + auto area = new Area(ah); + + // The widget should get as much space as possible. + area->setSizePolicy({QSizePolicy::MinimumExpanding,QSizePolicy::MinimumExpanding}); + + return uiAllocQt5ControlType(uiArea,area,uiQt5Control::DeleteControlOnQObjectFree); +} + +uiArea *uiNewScrollingArea(uiAreaHandler *ah, intmax_t width, intmax_t height) +{ + auto area = new Area(ah); + area->setMinimumSize({(int)width,(int)height}); + + auto scrollArea = new QScrollArea; + scrollArea->setBackgroundRole(QPalette::Dark); + scrollArea->setWidget(area); + + return uiAllocQt5ControlType(uiArea,scrollArea,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/box.cpp b/qt5/box.cpp new file mode 100644 index 00000000..7ad85b65 --- /dev/null +++ b/qt5/box.cpp @@ -0,0 +1,62 @@ + +#include "uipriv_qt5.hpp" + +#include +#include + +#include +#include + +struct uiBox : public uiQt5Control {}; + +void uiBoxAppend(uiBox *b, uiControl *c, int stretchy) +{ + if (auto layoutBox = uiValidateAndCastObjTo(b)) { + auto obj = uiValidateAndCastObjTo(c); + + if (auto layout = qobject_cast(obj)) { + layoutBox->addLayout(layout, stretchy); + } else if (auto widget = qobject_cast(obj)) { + layoutBox->addWidget(widget); + } else { + qWarning("object is neither layout nor widget"); + } + } +} + +void uiBoxDelete(uiBox *b, uintmax_t index) +{ + if (auto layoutBox = uiValidateAndCastObjTo(b)) { + if (index < (uint)layoutBox->count()) { + delete layoutBox->takeAt(index); + } + } +} + +int uiBoxPadded(uiBox *b) +{ + if (auto layoutBox = uiValidateAndCastObjTo(b)) { + layoutBox->spacing(); + } + return 0; +} + +void uiBoxSetPadded(uiBox *b, int padded) +{ + if (auto layoutBox = uiValidateAndCastObjTo(b)) { + layoutBox->setSpacing(padded); + } +} + +uiBox *uiNewHorizontalBox(void) +{ + return uiAllocQt5ControlType(uiBox, new QHBoxLayout, uiQt5Control::DeleteControlOnQObjectFree); +} + +uiBox *uiNewVerticalBox(void) +{ + auto layout = new QVBoxLayout; + layout->setAlignment(Qt::AlignTop); + + return uiAllocQt5ControlType(uiBox, layout, uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/button.cpp b/qt5/button.cpp new file mode 100644 index 00000000..60695d0a --- /dev/null +++ b/qt5/button.cpp @@ -0,0 +1,39 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiButton : public uiQt5Control {}; + +char *uiButtonText(uiButton *b) +{ + if (auto button = uiValidateAndCastObjTo(b)) { + return uiQt5StrdupQString(button->text()); + } + return nullptr; +} + +void uiButtonSetText(uiButton *b, const char *text) +{ + if (auto button = uiValidateAndCastObjTo(b)) { + button->setText(QString::fromUtf8(text)); + } +} + +void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data) +{ + if (auto button = uiValidateAndCastObjTo(b)) { + QObject::connect(button, &QPushButton::clicked, button, [f,b,data]{ + f(b,data); + }, Qt::UniqueConnection); + } +} + +uiButton *uiNewButton(const char *text) +{ + auto button = new QPushButton(QString::fromUtf8(text)); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiButton,button,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/checkbox.cpp b/qt5/checkbox.cpp new file mode 100644 index 00000000..7b515baa --- /dev/null +++ b/qt5/checkbox.cpp @@ -0,0 +1,54 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiCheckbox : public uiQt5Control {}; + +char *uiCheckboxText(uiCheckbox *c) +{ + if (auto checkBox = uiValidateAndCastObjTo(c)) { + return uiQt5StrdupQString(checkBox->text()); + } + return nullptr; +} + +void uiCheckboxSetText(uiCheckbox *c, const char *text) +{ + if (auto checkBox = uiValidateAndCastObjTo(c)) { + checkBox->setText(QString::fromUtf8(text)); + } +} + +void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data) +{ + if (auto checkBox = uiValidateAndCastObjTo(c)) { + QObject::connect(checkBox, &QCheckBox::toggled, checkBox, [f,c,data]{ + f(c,data); + }, Qt::UniqueConnection); + } +} + +int uiCheckboxChecked(uiCheckbox *c) +{ + if (auto checkBox = uiValidateAndCastObjTo(c)) { + return checkBox->isChecked(); + } + return 0; +} + +void uiCheckboxSetChecked(uiCheckbox *c, int checked) +{ + if (auto checkBox = uiValidateAndCastObjTo(c)) { + checkBox->setChecked(checked); + } +} + +uiCheckbox *uiNewCheckbox(const char *text) +{ + auto checkBox = new QCheckBox(QString::fromUtf8(text)); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiCheckbox,checkBox,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/colorbutton.cpp b/qt5/colorbutton.cpp new file mode 100644 index 00000000..53264007 --- /dev/null +++ b/qt5/colorbutton.cpp @@ -0,0 +1,69 @@ +#include "uipriv_qt5.hpp" + +#include +#include + +struct uiColorButton : public uiQt5Control {}; + +static QColorDialog *findColorDialog(uiColorButton *b) +{ + if (auto pushButton = uiValidateAndCastObjTo(b)) { + if (auto colorDialog = pushButton->findChild()) { + return colorDialog; + } + } + qWarning("colorDialog not found?!"); + return nullptr; +} + +void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a) +{ + if (auto colorDialog = findColorDialog(b)) { + auto color = colorDialog->currentColor(); + *r = color.redF(); + *g = color.greenF(); + *bl = color.blueF(); + *a = color.alphaF(); + } +} + +void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a) +{ + if (auto colorDialog = findColorDialog(b)) { + auto color = QColor::fromRgbF(r,g,bl,a); + colorDialog->setCurrentColor(color); + emit colorDialog->colorSelected(color); // hacky? + } +} + +void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data) +{ + if (auto colorDialog = findColorDialog(b)) { + QObject::connect(colorDialog, &QColorDialog::colorSelected, colorDialog, [f,b,data]{ + f(b,data); + }, Qt::UniqueConnection); + } +} + +uiColorButton *uiNewColorButton(void) +{ + auto pushButton = new QPushButton; + pushButton->setObjectName("colorButton"); // so we can target just this button with stylesheet + + // persisting this dialog in the background simplies things, but not a good partice + auto colorDialog = new QColorDialog(pushButton); + + auto updateColor = [pushButton](const QColor &color) { + // quick and dirty, probably not ideal, doesn't show transperency in anyway + auto qssColorString = QStringLiteral("rgb(%1,%2,%3)").arg(color.red()).arg(color.green()).arg(color.blue()); + pushButton->setStyleSheet(QStringLiteral("#colorButton {background-color: %1;}").arg(qssColorString)); + }; + QObject::connect(colorDialog, &QColorDialog::colorSelected, pushButton, updateColor); + updateColor({}); // set initial color (black) + + QObject::connect(pushButton, &QPushButton::clicked, colorDialog, &QColorDialog::show); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiColorButton,pushButton,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/combobox.cpp b/qt5/combobox.cpp new file mode 100644 index 00000000..06bac022 --- /dev/null +++ b/qt5/combobox.cpp @@ -0,0 +1,48 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiCombobox : public uiQt5Control {}; + +void uiComboboxAppend(uiCombobox *c, const char *text) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + comboBox->addItem(QString::fromUtf8(text)); + } +} + +intmax_t uiComboboxSelected(uiCombobox *c) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + return comboBox->currentIndex(); + } + return -1; +} + +void uiComboboxSetSelected(uiCombobox *c, intmax_t n) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + comboBox->setCurrentIndex(n); + } +} + +void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + // disambiguation of overloaded function + void (QComboBox:: *currentIndexChanged)(int) = &QComboBox::currentIndexChanged; + QObject::connect(comboBox, currentIndexChanged, comboBox, [f,c,data]{ + f(c,data); + }, Qt::UniqueConnection); + } +} + +uiCombobox *uiNewCombobox(void) +{ + auto comboBox = new QComboBox; + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiCombobox,comboBox,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/control.cpp b/qt5/control.cpp new file mode 100644 index 00000000..38e03988 --- /dev/null +++ b/qt5/control.cpp @@ -0,0 +1,135 @@ + +#include "uipriv_qt5.hpp" + +#include +#include + +#define uiQt5ControlSignature 0x51743578 // 'Qt5x'; + +Q_DECLARE_METATYPE(uiQt5Control *) + +uiQt5Control *uiValidateQt5Control(uiControl *control) +{ + if (control == nullptr) { + qCritical("Called with uiControl == nullptr"); + return nullptr; + } + if (control->Signature == uiQt5ControlSignature) { + qCritical("Called with uiControl (%p) that does not contain correct uiQt5ControlSignature? " + "Corruption or incorrect object passed in!", (void*)control); + return nullptr; + } + + auto qt5Control = static_cast(control);; + if (qt5Control->qobject == nullptr) { + if ((qt5Control->flags & uiQt5Control::SuppressValidatationNag) == 0) { + qCritical("Called with uiControl (%p) that doesn't have QObject (missing or already destroyed)", (void *)control); + } + return nullptr; + } + + return qt5Control; +} + +uiQt5Control *uiFindQt5ControlForQObject(const QObject *qobject) +{ + if (qobject) { + return qobject->property("uiQt5Control").value(); + } + return nullptr; +} + +uiQt5Control *uiAllocQt5Control(uint32_t typesig, const char *typenamestr, QObject *qobject, uint32_t flags) +{ + auto control = (uiQt5Control *)uiAllocControl(sizeof(uiQt5Control), uiQt5ControlSignature, typesig, typenamestr); + control->qobject = qobject; + control->flags = flags; + qobject->setProperty("uiQt5Control", QVariant::fromValue(control)); + QObject::connect(qobject, &QWidget::destroyed, qobject, [control]{ + control->qobject = nullptr; + if ((control->flags & uiQt5Control::DeleteControlOnQObjectFree) == 1) { + // supress warning about the QObject being already deleted + control->flags |= uiQt5Control::SuppressValidatationNag; + uiFreeControl(control); + } + }); + + control->Destroy = [](uiControl *control) { + if (auto qt5Control = uiValidateQt5Control(control)) { + // consider synchronous destruction for simplicity? this is safer however depending on the caller state + qt5Control->qobject->deleteLater(); + qt5Control->qobject->setProperty("uiQt5Control", {}); + + // disconnect the destroyed signal and explicity set widget to null since the control + // will be gone when the actual widget destruction happens + QObject::disconnect(qt5Control->qobject, &QWidget::destroyed, qt5Control->qobject, nullptr); + qt5Control->qobject = nullptr; + + uiFreeControl(control); + } + }; + control->Handle = [](uiControl *control) -> uintptr_t { + if (auto qt5Control = uiValidateQt5Control(control)) { + qWarning("VERIFY Handle usage"); + return (uintptr_t)qt5Control->qobject; + } + return 0; + }; + control->Parent = [](uiControl *control) -> uiControl * { + if (auto qt5Control = uiValidateQt5Control(control)) { + qWarning("TODO Parent"); + Q_UNUSED(qt5Control); + return nullptr; + } + return nullptr; + }; + control->SetParent = [](uiControl *control, uiControl *parent) { + if (auto qt5Control = uiValidateQt5Control(control)) { + Q_UNUSED(parent); + qWarning("TODO SetParent"); + Q_UNUSED(qt5Control); + } + }; + control->Toplevel = [](uiControl *control) -> int { + if (auto qt5Control = uiValidateQt5Control(control)) { + qWarning("TODO Toplevel"); + Q_UNUSED(qt5Control); + return 0; + } + return 0; + }; + control->Visible = [](uiControl *control) -> int { + if (auto widget = uiValidateAndCastObjTo(control)) { + return widget->isVisible(); + } + return 0; + }; + control->Show = [](uiControl *control) { + if (auto widget = uiValidateAndCastObjTo(control)) { + return widget->show(); + } + }; + control->Hide = [](uiControl *control) { + if (auto widget = uiValidateAndCastObjTo(control)) { + return widget->hide(); + } + }; + control->Enabled = [](uiControl *control) -> int { + if (auto widget = uiValidateAndCastObjTo(control)) { + return widget->isEnabled(); + } + return 0; + }; + control->Enable = [](uiControl *control) { + if (auto widget = uiValidateAndCastObjTo(control)) { + return widget->setEnabled(true); + } + }; + control->Disable = [](uiControl *control) { + if (auto widget = uiValidateAndCastObjTo(control)) { + return widget->setEnabled(false); + } + }; + + return control; +} diff --git a/qt5/datetimepicker.cpp b/qt5/datetimepicker.cpp new file mode 100644 index 00000000..c217f035 --- /dev/null +++ b/qt5/datetimepicker.cpp @@ -0,0 +1,37 @@ + +#include "uipriv_qt5.hpp" + +//#include // TODO: for dropdown + +#include +#include +#include + +struct uiDateTimePicker : public uiQt5Control {}; + +uiDateTimePicker *uiNewDateTimePicker(void) +{ + auto dateEdit = new QDateEdit(QDate::currentDate()); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiDateTimePicker,dateEdit,uiQt5Control::DeleteControlOnQObjectFree); +} + +uiDateTimePicker *uiNewDatePicker(void) +{ + auto timeEdit = new QTimeEdit(QTime::currentTime()); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiDateTimePicker,timeEdit,uiQt5Control::DeleteControlOnQObjectFree); +} + +uiDateTimePicker *uiNewTimePicker(void) +{ + auto dateTimeEdit = new QDateTimeEdit(QDateTime::currentDateTime()); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiDateTimePicker,dateTimeEdit,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/debug.cpp b/qt5/debug.cpp new file mode 100644 index 00000000..81fe826b --- /dev/null +++ b/qt5/debug.cpp @@ -0,0 +1,9 @@ + +#include "uipriv_qt5.hpp" + +#include + +void realbug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap) +{ + qFatal("[libui] %s:%s:%s() %s%s", file, line, func, prefix, qPrintable(QString::vasprintf(format,ap))); +} diff --git a/qt5/draw.cpp b/qt5/draw.cpp new file mode 100644 index 00000000..c3211d1f --- /dev/null +++ b/qt5/draw.cpp @@ -0,0 +1,113 @@ + +#include "uipriv_qt5.hpp" + +#include "draw.hpp" + +static QGradient uiDrawBrushToQGradient(const uiDrawBrush *b, QGradient gradient) +{ + for (size_t i=0; iNumStops; ++i) { + const auto &stop = b->Stops[i]; + const auto color = QColor::fromRgbF(stop.R, stop.G, stop.B, stop.A); + gradient.setColorAt(stop.Pos, color); + } + return gradient; +} + +static QBrush uiDrawBrushToQBrush(const uiDrawBrush *b) +{ + switch (b->Type) + { + case uiDrawBrushTypeSolid: + return {QColor::fromRgbF(b->R, b->G, b->B, b->A), Qt::SolidPattern}; + case uiDrawBrushTypeLinearGradient: + return uiDrawBrushToQGradient(b, QLinearGradient(b->X0, b->Y0, b->X1, b->Y1)); + case uiDrawBrushTypeRadialGradient: + return uiDrawBrushToQGradient(b, QRadialGradient(b->X0, b->Y0, b->OuterRadius, b->X1, b->Y1, 0)); + case uiDrawBrushTypeImage: + // not implemented in uiDrawBrush at time of writing + qWarning("TODO: uiDrawBrushTypeImage"); + break; + default: + qWarning("Unknown uiDrawBrushType: %d", b->Type); + } + // something noticable + return {Qt::magenta, Qt::DiagCrossPattern}; +} + +static QPen uiDrawStrokeParamsToQPen(const uiDrawStrokeParams *p, const QBrush &brush) +{ + QPen pen(brush, p->Thickness); + + switch (p->Cap) + { + case uiDrawLineCapFlat: + pen.setCapStyle(Qt::FlatCap); + break; + case uiDrawLineCapRound: + pen.setCapStyle(Qt::RoundCap); + break; + case uiDrawLineCapSquare: + pen.setCapStyle(Qt::SquareCap); + break; + default: + qWarning("Unknown uiDrawLineCap: %d", p->Cap); + } + + switch (p->Join) + { + case uiDrawLineJoinMiter: + pen.setJoinStyle(Qt::MiterJoin); + break; + case uiDrawLineJoinRound: + pen.setJoinStyle(Qt::RoundJoin); + break; + case uiDrawLineJoinBevel: + pen.setJoinStyle(Qt::BevelJoin); + break; + default: + qWarning("Unknown uiDrawLineJoin: %d", p->Join); + } + + pen.setMiterLimit(p->MiterLimit); + + if (p->NumDashes) { + pen.setDashOffset(p->DashPhase/10.0); + QVector pattern; + for (size_t i=0; iNumDashes; ++i) { + pattern.append(p->Dashes[i]/10.0); + } + pen.setDashPattern(pattern); + } + + return pen; +} + +void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p) +{ + c->strokePath(*path, uiDrawStrokeParamsToQPen(p, uiDrawBrushToQBrush(b))); +} + +void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b) +{ + c->fillPath(*path, uiDrawBrushToQBrush(b)); +} + +void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m) +{ + c->setTransform(m2t(m), true /* combine */); +} + +void uiDrawClip(uiDrawContext *c, uiDrawPath *path) +{ + c->setClipPath(*path); +} + +void uiDrawSave(uiDrawContext *c) +{ + c->save(); +} + +void uiDrawRestore(uiDrawContext *c) +{ + c->restore(); +} diff --git a/qt5/draw.hpp b/qt5/draw.hpp new file mode 100644 index 00000000..ca908b61 --- /dev/null +++ b/qt5/draw.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +typedef struct uiDrawContext uiDrawContext; + +struct uiDrawContext : public QPainter +{ + uiDrawContext(QPaintDevice *device) + : QPainter(device) + { + } +}; + +#include + +struct uiDrawPath : QPainterPath +{ + uiDrawPath(uiDrawFillMode mode) + { + switch (mode) + { + case uiDrawFillModeWinding: + setFillRule(Qt::WindingFill); + break; + case uiDrawFillModeAlternate: + setFillRule(Qt::OddEvenFill); + break; + default: + qWarning("Unknown uiDrawFillMode: %d", mode); + } + } +}; + +extern QTransform m2t(const uiDrawMatrix *m); + +struct uiDrawTextFont : public QFont +{ + uiDrawTextFont(const QFont &font) + : QFont(font) {} +}; diff --git a/qt5/drawmatrix.cpp b/qt5/drawmatrix.cpp new file mode 100644 index 00000000..f2d04515 --- /dev/null +++ b/qt5/drawmatrix.cpp @@ -0,0 +1,80 @@ + +#include "uipriv_qt5.hpp" + +#include "draw.hpp" + +QTransform m2t(const uiDrawMatrix *m) +{ + return QTransform(m->M11, m->M12, m->M21, m->M22, m->M31, m->M32); +} + +static void t2m(const QTransform &t, uiDrawMatrix *m) +{ + m->M11 = t.m11(); + m->M12 = t.m12(); + m->M21 = t.m21(); + m->M22 = t.m22(); + m->M31 = t.m31(); + m->M32 = t.m32(); +} + +void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y) +{ + auto t = m2t(m); + t.translate(x,y); + t2m(t,m); +} + +void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y) +{ + auto t = m2t(m); + t.translate(x,y); + t.scale(x,y); + t.translate(-x,-y); + t2m(t,m); +} + +void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount) +{ + auto t = m2t(m); + t.translate(x,y); + t.rotateRadians(amount); + t.translate(-x,-y); + t2m(t,m); +} + +void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount) +{ + auto t = m2t(m); + t.translate(x,y); + t.shear(xamount, yamount); + t.translate(-x,-y); + t2m(t,m); +} + +void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src) +{ + qWarning("TODO: %p, %p", (void *)dest, (void *)src); +} + +int uiDrawMatrixInvertible(uiDrawMatrix *m) +{ + qWarning("TODO: %p", (void *)m); + return 0; +} + +int uiDrawMatrixInvert(uiDrawMatrix *m) +{ + qWarning("TODO: %p", (void *)m); + return 0; +} + +void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y) +{ + qWarning("TODO: %p, %f %f", (void *)m, *x, *y); +} + +void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y) +{ + qWarning("TODO: %p, %f %f", (void *)m, *x, *y); +} diff --git a/qt5/drawpath.cpp b/qt5/drawpath.cpp new file mode 100644 index 00000000..12fb3701 --- /dev/null +++ b/qt5/drawpath.cpp @@ -0,0 +1,76 @@ + +#include "uipriv_qt5.hpp" + +#include "draw.hpp" + +#include + +uiDrawPath *uiDrawNewPath(uiDrawFillMode mode) +{ + return new uiDrawPath(mode); +} + +void uiDrawFreePath(uiDrawPath *p) +{ + delete p; +} + +void uiDrawPathNewFigure(uiDrawPath *p, double x, double y) +{ + p->moveTo(x,y); +} + +void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + qreal xStart = xCenter + radius*qCos(startAngle); + qreal yStart = yCenter + radius*qSin(startAngle); + p->moveTo(xStart, yStart); + if (negative) { + sweep = -(2*M_PI-sweep); + } + p->arcTo(xCenter-radius, yCenter-radius, radius*2, radius*2, -qRadiansToDegrees(startAngle), -qRadiansToDegrees(sweep)); +} + +void uiDrawPathLineTo(uiDrawPath *p, double x, double y) +{ + p->lineTo(x,y); +} + +void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + if (negative) { + sweep = -(2*M_PI-sweep); + } + p->arcTo(xCenter-radius, yCenter-radius, radius*2, radius*2, -qRadiansToDegrees(startAngle), -qRadiansToDegrees(sweep)); +} + +void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY) +{ + p->cubicTo(c1x, c1y, c2x, c2y, endX, endY); +} + +void uiDrawPathCloseFigure(uiDrawPath *p) +{ + p->closeSubpath(); +} + +void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height) +{ + p->addRect(x,y,width,height); +} + +void uiDrawPathEnd(uiDrawPath *p) +{ + Q_UNUSED(p); +} + +uiDrawFillMode pathFillMode(uiDrawPath *path) +{ + switch (path->fillRule()) + { + case Qt::WindingFill: return uiDrawFillModeWinding; + case Qt::OddEvenFill: return uiDrawFillModeAlternate; + } + Q_UNREACHABLE(); + return 0; +} diff --git a/qt5/drawtext.cpp b/qt5/drawtext.cpp new file mode 100644 index 00000000..27477040 --- /dev/null +++ b/qt5/drawtext.cpp @@ -0,0 +1,190 @@ + +#include "uipriv_qt5.hpp" + +#include "draw.hpp" // draw context + +#include +#include +#include + +struct uiDrawFontFamilies : public QStringList +{ + uiDrawFontFamilies(const QStringList &stringList) + : QStringList(stringList) {} +}; + +struct uiDrawTextLayout : public QTextLayout +{ + uiDrawTextLayout(const QString& text, const QFont &font, double width) + : QTextLayout(text, font) + , width_(width) + { +// qWarning("TODO: figure out wtf width is for: %f", width_); + } + double width_ = 0.0; +}; + +uiDrawFontFamilies *uiDrawListFontFamilies(void) +{ + return new uiDrawFontFamilies(QFontDatabase().families()); +} + +uintmax_t uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff) +{ + return ff->count(); +} + +char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, uintmax_t n) +{ + return uiQt5StrdupQString(ff->value(n)); +} + +void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff) +{ + delete ff; +} + +static int uiDrawTextWeightToFontWeight(uiDrawTextWeight weight) +{ + switch (weight) + { + case uiDrawTextWeightThin: return QFont::Thin; + case uiDrawTextWeightUltraLight: return QFont::ExtraLight; + case uiDrawTextWeightLight: return QFont::Light; + case uiDrawTextWeightBook: return (QFont::Light+QFont::Normal)/2; + case uiDrawTextWeightNormal: return QFont::Normal; + case uiDrawTextWeightMedium: return QFont::Medium; + case uiDrawTextWeightSemiBold: return QFont::DemiBold; + case uiDrawTextWeightBold: return QFont::Bold; + case uiDrawTextWeightUtraBold: return QFont::ExtraBold; + case uiDrawTextWeightHeavy: return (QFont::ExtraBold+QFont::Black)/2; + case uiDrawTextWeightUltraHeavy: return QFont::Black; + default: + qWarning("Unknown uiDrawTextWeight: %d", weight); + } + return QFont::Normal; +} + +static int uiDrawTextItalicToFontStyle(uiDrawTextItalic italic) +{ + switch(italic) + { + case uiDrawTextItalicNormal: return QFont::StyleNormal; + case uiDrawTextItalicOblique: return QFont::StyleOblique; + case uiDrawTextItalicItalic: return QFont::StyleItalic; + default: + qWarning("Unknown uiDrawTextItalic: %d", italic); + } + return QFont::StyleNormal; +} + +static int uiDrawTextStretchToFont(uiDrawTextStretch stretch) +{ + switch(stretch) + { + case uiDrawTextStretchUltraCondensed: return QFont::UltraCondensed; + case uiDrawTextStretchExtraCondensed: return QFont::ExtraCondensed; + case uiDrawTextStretchCondensed: return QFont::Condensed; + case uiDrawTextStretchSemiCondensed: return QFont::SemiCondensed; + case uiDrawTextStretchNormal: return QFont::Unstretched; + case uiDrawTextStretchSemiExpanded: return QFont::SemiExpanded; + case uiDrawTextStretchExpanded: return QFont::Expanded; + case uiDrawTextStretchExtraExpanded: return QFont::ExtraExpanded; + case uiDrawTextStretchUltraExpanded: return QFont::UltraExpanded; + default: + qWarning("Unknown uiDrawTextStretch: %d", stretch); + } + return QFont::Unstretched; +} + +uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc) +{ + QFont font(QString::fromUtf8(desc->Family)); + font.setPointSizeF(desc->Size); + font.setWeight(uiDrawTextWeightToFontWeight(desc->Weight)); + font.setItalic(uiDrawTextItalicToFontStyle(desc->Italic)); + font.setStretch(uiDrawTextStretchToFont(desc->Stretch)); + + return new uiDrawTextFont(font); +} + +void uiDrawFreeTextFont(uiDrawTextFont *font) +{ + delete font; +} + +uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font) +{ + qWarning("TODO: %p", (void *)font); + return 0; +} + +void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc) +{ + qWarning("TODO: %p %p", (void *)font, (void *)desc); +} + +void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics) +{ + if (font) { + QFontMetricsF fontMetrics(*font); + metrics->Ascent = fontMetrics.ascent(); + metrics->Descent = fontMetrics.descent(); + metrics->Leading = fontMetrics.leading(); + metrics->UnderlinePos = fontMetrics.underlinePos(); + metrics->UnderlineThickness = 0; // TODO? + } +} + +uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width) +{ + return new uiDrawTextLayout(QString::fromUtf8(text), *defaultFont, width); +} + +void uiDrawFreeTextLayout(uiDrawTextLayout *layout) +{ + delete layout; +} + +void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width) +{ + layout->width_ = width; +} + +static void runLayouting(uiDrawTextLayout *layout) +{ + layout->setCacheEnabled(true); + layout->beginLayout(); + forever { + QTextLine line = layout->createLine(); + if (!line.isValid()) { + break; + } + // TODO: shift by line height? + } + layout->endLayout(); +} + +void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height) +{ + runLayouting(layout); + auto boundingRect = layout->boundingRect(); + *width = boundingRect.width(); + *height = boundingRect.height(); +} + +void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout) +{ + runLayouting(layout); + layout->draw(c, {x,y}); +} + +void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, intmax_t startChar, intmax_t endChar, double r, double g, double b, double a) +{ + // ineffective..? but works + auto additionalFormats = layout->additionalFormats(); + QTextCharFormat format; + format.setForeground(QColor::fromRgbF(r,g,b,a)); + additionalFormats.append(QTextLayout::FormatRange{(int)startChar,(int)(1+endChar-startChar),format}); + layout->setAdditionalFormats(additionalFormats); +} diff --git a/qt5/editablecombo.cpp b/qt5/editablecombo.cpp new file mode 100644 index 00000000..7e7c2336 --- /dev/null +++ b/qt5/editablecombo.cpp @@ -0,0 +1,65 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiEditableCombobox : public uiQt5Control {}; + + +void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + comboBox->addItem(QString::fromUtf8(text)); + } +} + +intmax_t uiEditableComboboxSelected(uiEditableCombobox *c) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + return comboBox->currentIndex(); + } + return -1; +} + +void uiEditableComboboxSetSelected(uiEditableCombobox *c, intmax_t n) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + return comboBox->setCurrentIndex(n); + } +} + +char *uiEditableComboboxText(uiEditableCombobox *c) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + return uiQt5StrdupQString(comboBox->currentText()); + } + return nullptr; +} + +void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + comboBox->setCurrentText(QString::fromUtf8(text)); + } +} + +void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data) +{ + if (auto comboBox = uiValidateAndCastObjTo(c)) { + // disambiguation of overloaded function + void (QComboBox:: *currentIndexChanged)(const QString &) = &QComboBox::currentIndexChanged; + QObject::connect(comboBox, currentIndexChanged, comboBox, [f,c,data]{ + f(c,data); + }, Qt::UniqueConnection); + } +} + +uiEditableCombobox *uiNewEditableCombobox(void) +{ + auto comboBox = new QComboBox; + comboBox->setEditable(true); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiEditableCombobox,comboBox,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/entry.cpp b/qt5/entry.cpp new file mode 100644 index 00000000..ff9e659a --- /dev/null +++ b/qt5/entry.cpp @@ -0,0 +1,55 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiEntry : uiQt5Control {}; + +char *uiEntryText(uiEntry *e) +{ + if (auto lineEdit = uiValidateAndCastObjTo(e)) { + return uiQt5StrdupQString(lineEdit->text()); + } + return nullptr; +} + +void uiEntrySetText(uiEntry *e, const char *text) +{ + if (auto lineEdit = uiValidateAndCastObjTo(e)) { + lineEdit->setText(QString::fromUtf8(text)); + } +} + +void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data) +{ + if (auto lineEdit = uiValidateAndCastObjTo(e)) { + // Unlike textChanged(), this signal is not emitted when the text is changed programmatically + QObject::connect(lineEdit, &QLineEdit::textChanged, lineEdit, [f,e,data]{ + f(e,data); + }, Qt::UniqueConnection); + } +} + +int uiEntryReadOnly(uiEntry *e) +{ + if (auto lineEdit = uiValidateAndCastObjTo(e)) { + return lineEdit->isReadOnly(); + } + return false; +} + +void uiEntrySetReadOnly(uiEntry *e, int readonly) +{ + if (auto lineEdit = uiValidateAndCastObjTo(e)) { + lineEdit->setReadOnly(readonly); + } +} + +uiEntry *uiNewEntry(void) +{ + auto lineEdit = new QLineEdit; + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiEntry,lineEdit,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/fontbutton.cpp b/qt5/fontbutton.cpp new file mode 100644 index 00000000..50b87576 --- /dev/null +++ b/qt5/fontbutton.cpp @@ -0,0 +1,56 @@ + +#include "uipriv_qt5.hpp" + +#include "draw.hpp" + +// another way would be to return a QFontComboBox + QSpinBox (for font size), +// but this is more true the control name +#include +#include + +struct uiFontButton : public uiQt5Control {}; + +uiDrawTextFont *uiFontButtonFont(uiFontButton *b) +{ + if (auto pushButton = uiValidateAndCastObjTo(b)) { + if (auto fontDialog = pushButton->findChild()) { + return new uiDrawTextFont(fontDialog->currentFont()); + } + } + return nullptr; +} + +void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data) +{ + if (auto pushButton = uiValidateAndCastObjTo(b)) { + if (auto fontDialog = pushButton->findChild()) { + QObject::connect(fontDialog, &QFontDialog::fontSelected, fontDialog, [f,b,data]{ + f(b,data); + }, Qt::UniqueConnection); + } + } +} + +uiFontButton *uiNewFontButton(void) +{ + auto pushButton = new QPushButton; + + // persisting this dialog in the background simplies things, but not a good partice + auto fontDialog = new QFontDialog(pushButton); + + auto updateFont = [pushButton](const QFont &font) { + auto family = font.family(); + auto pointSize = font.pointSize(); // or pixelSize..? + pushButton->setText(QStringLiteral("%1 %2").arg(family).arg(pointSize)); + }; + QObject::connect(fontDialog, &QFontDialog::fontSelected, pushButton, updateFont); + QObject::connect(pushButton, &QPushButton::clicked, fontDialog, &QFontDialog::show); + + QFont font; font.setPointSize(12); + updateFont(font); + fontDialog->setCurrentFont(font); // set initial font (system font) + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiFontButton,pushButton,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/group.cpp b/qt5/group.cpp new file mode 100644 index 00000000..ff9f68af --- /dev/null +++ b/qt5/group.cpp @@ -0,0 +1,63 @@ + +#include "uipriv_qt5.hpp" + +#include + +#include + +struct uiGroup : public uiQt5Control {}; + +char *uiGroupTitle(uiGroup *g) +{ + if (auto groupBox = uiValidateAndCastObjTo(g)) { + return uiQt5StrdupQString(groupBox->title()); + } + return nullptr; +} + +void uiGroupSetTitle(uiGroup *g, const char *text) +{ + if (auto groupBox = uiValidateAndCastObjTo(g)) { + groupBox->setTitle(QString::fromUtf8(text)); + } +} + +void uiGroupSetChild(uiGroup *g, uiControl *child) +{ + if (auto groupBox = uiValidateAndCastObjTo(g)) { + auto obj = uiValidateAndCastObjTo(child); + if (groupBox->layout()) { + groupBox->layout()->deleteLater(); + } + if (auto layout = qobject_cast(obj)) { + groupBox->setLayout(layout); + } else if (auto widget = qobject_cast(obj)) { + auto layout = new QVBoxLayout; + layout->setMargin(0); // ? + layout->addWidget(widget); + groupBox->setLayout(layout); + } else { + qWarning("object is neither layout nor widget"); + } + } +} + +int uiGroupMargined(uiGroup *g) +{ + qWarning("TODO: %p", (void*)g); + return 0; +} + +void uiGroupSetMargined(uiGroup *g, int margined) +{ + qWarning("TODO: %p, %d", (void*)g, margined); +} + +uiGroup *uiNewGroup(const char *text) +{ + auto groupBox = new QGroupBox(QString::fromUtf8(text)); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiGroup,groupBox,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/label.cpp b/qt5/label.cpp new file mode 100644 index 00000000..9eccec48 --- /dev/null +++ b/qt5/label.cpp @@ -0,0 +1,30 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiLabel : public uiQt5Control {}; + +char *uiLabelText(uiLabel *l) +{ + if (auto label = uiValidateAndCastObjTo(l)) { + return uiQt5StrdupQString(label->text()); + } + return nullptr; +} + +void uiLabelSetText(uiLabel *l, const char *text) +{ + if (auto label = uiValidateAndCastObjTo(l)) { + label->setText(QString::fromUtf8(text)); + } +} + +uiLabel *uiNewLabel(const char *text) +{ + auto label = new QLabel(QString::fromUtf8(text)); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiLabel,label,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/main.cpp b/qt5/main.cpp new file mode 100644 index 00000000..99d77d0d --- /dev/null +++ b/qt5/main.cpp @@ -0,0 +1,138 @@ + +#include "uipriv_qt5.hpp" + +#include +#include +#include +#include + +#include + +const char *styleSheet = R"qcss( +QGroupBox { + font: bold; + color: #333; + /*border: none;*/ +} +)qcss"; + +const char *uiInit(uiInitOptions *o) +{ + Q_UNUSED(o); // don't care about something that isn't used + + if (QCoreApplication::instance() != nullptr) { + return "another QApplication instance already exists"; + } + + static int argc = 0; + static const char *argv[] = {"libui"}; + auto app = new QApplication(argc,(char **)argv); + + if (app == nullptr) { + return "failed to allocate new QApplication"; + } + + qSetMessagePattern("%{file}:%{line} %{function}(): %{message}"); + + // few consistency things + app->setQuitOnLastWindowClosed(false); + app->setStyleSheet(styleSheet); + + initAlloc(); + + return NULL; +} + +void uiUninit(void) +{ + auto app = static_cast(QCoreApplication::instance()); + + // not strictly necessary, just so we don't get needless nag from uninitAlloc() + // becasue of cascading it's not possible to iterate over the list (copy) + while (app->topLevelWidgets().count()) { + delete app->topLevelWidgets().first(); + } + + uninitAlloc(); + + delete app; +} + +void uiFreeInitError(const char *err) +{ + Q_UNUSED(err); // we only return static constants for errors +} + +void uiMain(void) +{ + if (QApplication::instance()) { + QApplication::instance()->exec(); + } +} + +int uiMainStep(int wait) +{ + if (QApplication::instance()) { + if (wait) { + // processEvents will always return immediately if there are no + // immediate events to process + + // we would have to install a event listener and run in while loop + // that seems very questionable.. don't understand the use case + qWarning("TODO: wait - no equivalent?"); + } + QApplication::instance()->processEvents(); + } + return 0; +} + +void uiQuit(void) +{ + QTimer::singleShot(0,QApplication::instance(), &QApplication::quit); +} + +void uiQueueMain(void (*f)(void *data), void *data) +{ + if (!QCoreApplication::instance()) { + // no instance...? + return; + } + + if (QThread::currentThread()->eventDispatcher()) { + // the simple way to queue something to run on main thread + QTimer::singleShot(0,QCoreApplication::instance(), [f,data]{ + f(data); + }); + } else { + // very dirty workaround, spawn a new thread.. with event dispatcher to deliver this + + // this could be done with custom signal, but since I decided to do + // this without using moc this is the simplest solution I could think of. + + // sort of works, but not very light weight and will block to ensure resource cleanup + class Thread : public QThread + { + std::function taskForMain_; + + void run() + { + QTimer::singleShot(0,QCoreApplication::instance(), taskForMain_); + exec(); // start event loop + } + public: + Thread(std::function taskForMain) + : taskForMain_(taskForMain) + { + start(); + } + ~Thread() + { + quit(); + wait(); + } + }; + Thread([f,data]{ + f(data); + }); + } +} diff --git a/qt5/menu.cpp b/qt5/menu.cpp new file mode 100644 index 00000000..372220d2 --- /dev/null +++ b/qt5/menu.cpp @@ -0,0 +1,162 @@ + +#include "uipriv_qt5.hpp" + +#include + +#include +#include + +struct uiMenu : public uiQt5Control {}; + +#ifndef uiMenuSignature +#define uiMenuSignature 0x4d656e75 // 'Menu' +#endif + +struct uiMenuItem : public uiQt5Control {}; + +#ifndef uiMenuItemSignature +#define uiMenuItemSignature 0x4d654974 // 'MeIt' +#endif + +void uiMenuItemEnable(uiMenuItem *item) +{ + if (auto action = uiValidateAndCastObjTo(item)) { + action->setEnabled(true); + } +} + +void uiMenuItemDisable(uiMenuItem *item) +{ + if (auto action = uiValidateAndCastObjTo(item)) { + action->setEnabled(false); + } +} + +static uiWindow *uiQt5FindWindow(QObject *qobject) +{ + if (qobject) { + if (auto widget = qobject_cast(qobject)) { + if (auto topLevel = widget->topLevelWidget()) { + return static_cast(uiFindQt5ControlForQObject(topLevel)); + } + } + return uiQt5FindWindow(qobject->parent()); + } + return nullptr; +} + +static uiWindow *uiQt5FindWindow(uiControl *leaf) +{ + if (auto qobject = uiValidateAndCastObjTo(leaf)) { + return uiQt5FindWindow(qobject); + } + return nullptr; +} + +void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data) +{ + if (auto action = uiValidateAndCastObjTo(item)) { + QObject::connect(action,&QAction::triggered, action, [f,item,data]{ + auto window = uiQt5FindWindow(item); + f(item,window,data); + }, Qt::UniqueConnection); + } +} + +int uiMenuItemChecked(uiMenuItem *item) +{ + if (auto action = uiValidateAndCastObjTo(item)) { + return action->isChecked(); + } + return 0; +} + +void uiMenuItemSetChecked(uiMenuItem *item, int checked) +{ + if (auto action = uiValidateAndCastObjTo(item)) { + action->setChecked(checked); + } +} + +uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name) +{ + if (auto menu = uiValidateAndCastObjTo(m)) { + auto action = menu->addAction(name); + + action->setObjectName(name); // for debugging only + + return uiAllocQt5ControlType(uiMenuItem, action, uiQt5Control::DeleteControlOnQObjectFree); + } + return nullptr; +} + +uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name) +{ + auto menuItem = uiMenuAppendItem(m, name); + if (auto action = uiValidateAndCastObjTo(menuItem)) { + action->setCheckable(true); + } + return menuItem; +} + +uiMenuItem *uiMenuAppendQuitItem(uiMenu *m) +{ + uiMenuAppendSeparator(m); + auto menuItem = uiMenuAppendItem(m, "&Quit"); + if (auto action = uiValidateAndCastObjTo(menuItem)) { + action->setShortcut(QKeySequence::Quit); + } + uiMenuItemOnClicked(menuItem, [](uiMenuItem *, uiWindow *, void *) { uiQuit(); }, nullptr); + return menuItem; +} + +uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m) +{ + uiMenuAppendSeparator(m); + auto menuItem = uiMenuAppendItem(m, "&Preferences..."); + if (auto action = uiValidateAndCastObjTo(menuItem)) { + action->setShortcut(QKeySequence::Preferences); + } + return menuItem; +} + +uiMenuItem *uiMenuAppendAboutItem(uiMenu *m) +{ + uiMenuAppendSeparator(m); + return uiMenuAppendItem(m, "&About"); +} + +void uiMenuAppendSeparator(uiMenu *m) +{ + if (auto menu = uiValidateAndCastObjTo(m)) { + menu->addSeparator(); + } +} + +QMenuBar *uiQt5FindMainMenuBar() +{ + auto app = static_cast(QCoreApplication::instance()); + for (auto widget : app->topLevelWidgets()) { + // see if the widget is the menubar + if (auto menuBar = qobject_cast(widget)) { + return menuBar; + } + // see if the widget owns the menubar + if (auto menuBar = widget->findChild()) { + return menuBar; + } + } + // not found, create + return new QMenuBar; +} + +uiMenu *uiNewMenu(const char *name) +{ + auto menuBar = uiQt5FindMainMenuBar(); + auto menu = new QMenu(QString::fromUtf8(name), menuBar); + menuBar->addMenu(menu); + + menu->setObjectName(name); // for debugging only + + return uiAllocQt5ControlType(uiMenu, menu, uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/multilineentry.cpp b/qt5/multilineentry.cpp new file mode 100644 index 00000000..cb75a9e4 --- /dev/null +++ b/qt5/multilineentry.cpp @@ -0,0 +1,79 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiMultilineEntry : public uiQt5Control {}; + +char *uiMultilineEntryText(uiMultilineEntry *e) +{ + if (auto textEdit = uiValidateAndCastObjTo(e)) { + return uiQt5StrdupQString(textEdit->toPlainText()); + } + return nullptr; +} + +void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text) +{ + if (auto textEdit = uiValidateAndCastObjTo(e)) { + textEdit->setPlainText(QString::fromUtf8(text)); + } +} + +void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text) +{ + if (auto textEdit = uiValidateAndCastObjTo(e)) { + // append is least interferring (keeps selection and cursor), + // but will add newline by it self hence not exactly compatible + + // since the newline is implicitly inserted we need to remove + // the explicitly inserted one if we want the examples to look + // same, but this may cause other issues.. + auto oldBlockSignals = textEdit->blockSignals(true); + textEdit->append(QString::fromUtf8(text).trimmed()); + textEdit->blockSignals(oldBlockSignals); + } +} + +void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *e, void *data), void *data) +{ + if (auto textEdit = uiValidateAndCastObjTo(e)) { + QObject::connect(textEdit, &QTextEdit::textChanged, textEdit, [f,e,data]{ + f(e,data); + }, Qt::UniqueConnection); + } +} + +int uiMultilineEntryReadOnly(uiMultilineEntry *e) +{ + if (auto textEdit = uiValidateAndCastObjTo(e)) { + textEdit->isReadOnly(); + } + return 0; +} + +void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly) +{ + if (auto textEdit = uiValidateAndCastObjTo(e)) { + textEdit->setReadOnly(readonly); + } +} + +uiMultilineEntry *uiNewMultilineEntry(void) +{ + auto textEdit = new QTextEdit; + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiMultilineEntry,textEdit,uiQt5Control::DeleteControlOnQObjectFree); +} + +uiMultilineEntry *uiNewNonWrappingMultilineEntry(void) +{ + auto textEdit = new QTextEdit; + textEdit->setLineWrapMode(QTextEdit::NoWrap); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiMultilineEntry,textEdit,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/progressbar.cpp b/qt5/progressbar.cpp new file mode 100644 index 00000000..93167eaa --- /dev/null +++ b/qt5/progressbar.cpp @@ -0,0 +1,22 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiProgressBar : public uiQt5Control {}; + +void uiProgressBarSetValue(uiProgressBar *p, int value) +{ + if (auto progressBar = uiValidateAndCastObjTo(p)) { + return progressBar->setValue(value); + } +} + +uiProgressBar *uiNewProgressBar(void) +{ + auto progressBar = new QProgressBar; + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiProgressBar,progressBar,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/radiobuttons.cpp b/qt5/radiobuttons.cpp new file mode 100644 index 00000000..5ef62867 --- /dev/null +++ b/qt5/radiobuttons.cpp @@ -0,0 +1,28 @@ + +#include "uipriv_qt5.hpp" + +#include +#include + +struct uiRadioButtons : public uiQt5Control {}; + +void uiRadioButtonsAppend(uiRadioButtons *r, const char *text) +{ + if (auto layout = uiValidateAndCastObjTo(r)) { + auto radioButton = new QRadioButton(QString::fromUtf8(text)); + layout->addWidget(radioButton); + if (layout->count() == 1) { + radioButton->setChecked(true); + } + } +} + +uiRadioButtons *uiNewRadioButtons(void) +{ + // TODO: check does this need a QButtonGroup or is the layout sufficent? + auto layout = new QVBoxLayout; + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiRadioButtons,layout,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/separator.cpp b/qt5/separator.cpp new file mode 100644 index 00000000..2a9bc15e --- /dev/null +++ b/qt5/separator.cpp @@ -0,0 +1,16 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiSeparator : public uiQt5Control {}; + +uiSeparator *uiNewHorizontalSeparator(void) +{ + auto frame = new QFrame; + frame->setFrameStyle(QFrame::HLine | QFrame::Sunken); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiSeparator,frame,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/slider.cpp b/qt5/slider.cpp new file mode 100644 index 00000000..ae92d3ff --- /dev/null +++ b/qt5/slider.cpp @@ -0,0 +1,40 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiSlider : public uiQt5Control {}; + +intmax_t uiSliderValue(uiSlider *s) +{ + if (auto slider = uiValidateAndCastObjTo(s)) { + return slider->value(); + } + return 0; +} + +void uiSliderSetValue(uiSlider *s, intmax_t value) +{ + if (auto slider = uiValidateAndCastObjTo(s)) { + return slider->setValue((int)value); + } +} + +void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data) +{ + if (auto slider = uiValidateAndCastObjTo(s)) { + QObject::connect(slider, &QSlider::valueChanged, slider, [f,s,data]{ + f(s,data); + }, Qt::UniqueConnection); + } +} + +uiSlider *uiNewSlider(intmax_t min, intmax_t max) +{ + auto slider = new QSlider(Qt::Horizontal); + slider->setRange(qMin(min,max), qMax(min,max)); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiSlider,slider,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/spinbox.cpp b/qt5/spinbox.cpp new file mode 100644 index 00000000..155c655b --- /dev/null +++ b/qt5/spinbox.cpp @@ -0,0 +1,40 @@ + +#include "uipriv_qt5.hpp" + +#include + +struct uiSpinbox : public uiQt5Control {}; + +intmax_t uiSpinboxValue(uiSpinbox *s) +{ + if (auto spinBox = uiValidateAndCastObjTo(s)) { + return spinBox->value(); + } + return 0; +} + +void uiSpinboxSetValue(uiSpinbox *s, intmax_t value) +{ + if (auto spinBox = uiValidateAndCastObjTo(s)) { + spinBox->setValue(value); + } +} + +void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data) +{ + if (auto spinBox = uiValidateAndCastObjTo(s)) { + QObject::connect(spinBox, static_cast(&QSpinBox::valueChanged), spinBox, [f,s,data]{ + f(s,data); + }, Qt::UniqueConnection); + } +} + +uiSpinbox *uiNewSpinbox(intmax_t min, intmax_t max) +{ + auto spinBox = new QSpinBox; + spinBox->setRange(qMin(min,max),qMax(min,max)); + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiSpinbox,spinBox,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/stddialogs.cpp b/qt5/stddialogs.cpp new file mode 100644 index 00000000..7421d2ec --- /dev/null +++ b/qt5/stddialogs.cpp @@ -0,0 +1,29 @@ + +#include "uipriv_qt5.hpp" + +#include +#include + +char *uiOpenFile(uiWindow *parent) +{ + return uiQt5StrdupQString(QFileDialog::getOpenFileName(uiValidateAndCastObjTo(parent))); +} + +char *uiSaveFile(uiWindow *parent) +{ + return uiQt5StrdupQString(QFileDialog::getSaveFileName(uiValidateAndCastObjTo(parent))); +} + +void uiMsgBox(uiWindow *parent, const char *title, const char *description) +{ + QMessageBox::information(uiValidateAndCastObjTo(parent), + QString::fromUtf8(title), + QString::fromUtf8(description)); +} + +void uiMsgBoxError(uiWindow *parent, const char *title, const char *description) +{ + QMessageBox::warning(uiValidateAndCastObjTo(parent), + QString::fromUtf8(title), + QString::fromUtf8(description)); +} diff --git a/qt5/tab.cpp b/qt5/tab.cpp new file mode 100644 index 00000000..0169b835 --- /dev/null +++ b/qt5/tab.cpp @@ -0,0 +1,88 @@ + +#include "uipriv_qt5.hpp" + +#include +#include + +struct uiTab : public uiQt5Control {}; + +static void tabInsertAt(QTabWidget *tabWidget, const char *name, int n, uiControl *child) +{ + auto obj = uiValidateAndCastObjTo(child); + + if (auto layout = qobject_cast(obj)) { + auto widget = new QWidget; + widget->setLayout(layout); + tabWidget->insertTab(n, widget, QString::fromUtf8(name)); + } else if (auto widget = qobject_cast(obj)) { + tabWidget->insertTab(n, widget, QString::fromUtf8(name)); + } else { + qWarning("object is neither layout nor widget"); + } +} + +void uiTabAppend(uiTab *t, const char *name, uiControl *child) +{ + if (auto tabWidget = uiValidateAndCastObjTo(t)) { + tabInsertAt(tabWidget,name,tabWidget->count(),child); + } +} + +void uiTabInsertAt(uiTab *t, const char *name, uintmax_t n, uiControl *child) +{ + if (auto tabWidget = uiValidateAndCastObjTo(t)) { + tabInsertAt(tabWidget,name,qBound(0, n, tabWidget->count()),child); + } +} + +// reminder: badly named function, is remove not really a delete +void uiTabDelete(uiTab *t, uintmax_t n) +{ + if (auto tabWidget = uiValidateAndCastObjTo(t)) { + int i = qBound(0, n, tabWidget->count()); + if ((uintmax_t)i != n) { + qWarning("Bad index: %llu", (unsigned long long)n); + return; + } + auto widget = tabWidget->widget(i); + tabWidget->removeTab(i); + // check if it's a internal temp container widget we created + if (uiFindQt5ControlForQObject(widget) == nullptr) { + // widget is temp we created used as a container, separate the layout and then delete it + + // problem once a layout assigned to widget, it's not so easy to get it back..? + // one solution is to recreate the layout, but this needs some thought.. + // another solution is to never expose "layout" (uiBox), but always wrap them in containers.. + qWarning("FIXME: can't separate layout from temp widget"); +// delete widget; + } + } +} + +uintmax_t uiTabNumPages(uiTab *t) +{ + if (auto tabWidget = uiValidateAndCastObjTo(t)) { + return tabWidget->count(); + } + return 0; +} + +int uiTabMargined(uiTab *t, uintmax_t n) +{ + qWarning("TODO %p, %d", (void*)t, (int)n); + return 0; +} + +void uiTabSetMargined(uiTab *t, uintmax_t n, int margined) +{ + qWarning("TODO %p, %d, %d", (void*)t, (int)n, margined); +} + +uiTab *uiNewTab(void) +{ + auto tabWidget = new QTabWidget; + + // note styling is being set in main.cpp -> styleSheet + + return uiAllocQt5ControlType(uiTab,tabWidget,uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/qt5/text.cpp b/qt5/text.cpp new file mode 100644 index 00000000..034139de --- /dev/null +++ b/qt5/text.cpp @@ -0,0 +1,33 @@ + +#include "uipriv_qt5.hpp" + +#include + +static char *uiStrdupText(const char *s, int len) +{ + // Reminder: could use strndup, but it has poor portability + auto copy = (char *)malloc(len+1); + if (copy) { + memcpy(copy, s, len); + copy[len] = 0; + } else { + qCritical("Failed to allocate %d+1 bytes", len); + } + + return copy; +} + +char *uiQt5StrdupQString(const QString &string) +{ + if (string.isNull()) { + return nullptr; + } + + const auto utf8 = string.toUtf8(); + return uiStrdupText(utf8.constData(), utf8.length()); +} + +void uiFreeText(char *t) +{ + free(t); +} diff --git a/qt5/uipriv_qt5.hpp b/qt5/uipriv_qt5.hpp new file mode 100644 index 00000000..3dafe2a0 --- /dev/null +++ b/qt5/uipriv_qt5.hpp @@ -0,0 +1,52 @@ + +#ifndef __LIBUI_UIPRIV_QT5_HPP__ +#define __LIBUI_UIPRIV_QT5_HPP__ + +#include "../ui.h" +#include "../ui_qt5.h" +#include "../common/uipriv.h" + +extern void initAlloc(void); +extern void uninitAlloc(void); + +// text +class QString; +char *uiQt5StrdupQString(const QString &string); + +// control +class QObject; +struct uiQt5Control : public uiControl +{ + QObject *qobject; + enum { + DefaultFlags=0, + DeleteControlOnQObjectFree=0x1, + SuppressValidatationNag=0x2 + }; + uint32_t flags; +}; +extern uiQt5Control *uiValidateQt5Control(uiControl *control); + +// control to qobject +template +T *uiValidateAndCastObjTo(uiControl *control) +{ + if (auto qt5Control = uiValidateQt5Control(control)) { + return dynamic_cast(qt5Control->qobject); + } + return nullptr; +} +struct uiWindow : public uiQt5Control {}; + +// qobject to control +extern uiQt5Control *uiFindQt5ControlForQObject(const QObject *qobject); + +// alloc control +extern uiQt5Control *uiAllocQt5Control(uint32_t typesig, const char *typenamestr, QObject *qobject, uint32_t flags = uiQt5Control::DefaultFlags); +#define uiAllocQt5ControlType(type, widget, flags) static_cast(uiAllocQt5Control(type ## Signature, #type, widget, flags)) + +// menu +class QMenuBar; +extern QMenuBar *uiQt5FindMainMenuBar(); + +#endif // __LIBUI_UIPRIV_QT5_HPP__ diff --git a/qt5/window.cpp b/qt5/window.cpp new file mode 100644 index 00000000..e257d633 --- /dev/null +++ b/qt5/window.cpp @@ -0,0 +1,103 @@ + +#include "uipriv_qt5.hpp" + +#include + +#include +#include +#include + +#include + +class WindowWidget : public QMainWindow +{ +public: + std::function onClosing; + void closeEvent(QCloseEvent *event) + { + if (onClosing) { + onClosing(event); + } + } +}; + +char *uiWindowTitle(uiWindow *w) +{ + if (auto window = uiValidateAndCastObjTo(w)) { + return uiQt5StrdupQString(window->windowTitle()); + } + return nullptr; +} + +void uiWindowSetTitle(uiWindow *w, const char *title) +{ + if (auto window = uiValidateAndCastObjTo(w)) { + window->setWindowTitle(QString::fromUtf8(title)); + } +} + +void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data) +{ + if (auto window = uiValidateAndCastObjTo(w)) { + + if (f) { + window->onClosing = [f,w,data](QCloseEvent *event){ + if (f(w,data)) { + // eat the event and destroy the control + // normally such behavior would be achived with + // setAttribute(Qt::WA_DeleteOnClose, true) + // but we need to behave consistently + event->accept(); + w->Destroy(w); + } + }; + } else { + // clear callback + window->onClosing = nullptr; + } + } +} + +void uiWindowSetChild(uiWindow *w, uiControl *child) +{ + if (auto window = uiValidateAndCastObjTo(w)) { + auto obj = uiValidateAndCastObjTo(child); + if (window->centralWidget()) { + window->centralWidget()->deleteLater(); + } + if (auto layout = qobject_cast(obj)) { + auto widget = new QWidget; + widget->setLayout(layout); + window->setCentralWidget(widget); + } else if (auto widget = qobject_cast(obj)) { + window->setCentralWidget(widget); + } else { + qWarning("object is neither layout nor widget"); + } + } +} + +int uiWindowMargined(uiWindow *w) +{ + qWarning("TODO %p", (void*)w); + return 0; +} + +void uiWindowSetMargined(uiWindow *w, int margined) +{ + qWarning("TODO %p, %d", (void*)w, margined); +} + +uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar) +{ + auto window = new WindowWidget; + + window->setWindowTitle(title); + window->resize(width,height); + + if (hasMenubar) { + window->setMenuBar(uiQt5FindMainMenuBar()); + } + + return uiAllocQt5ControlType(uiWindow, window, uiQt5Control::DeleteControlOnQObjectFree); +} diff --git a/ui_qt5.h b/ui_qt5.h new file mode 100644 index 00000000..cd1a3e50 --- /dev/null +++ b/ui_qt5.h @@ -0,0 +1,7 @@ + +#ifndef __LIBUI_UI_QT5_H__ +#define __LIBUI_UI_QT5_H__ + + + +#endif // __LIBUI_UI_QT5_H__