├── .gitignore ├── LICENSE ├── README.md ├── application-enum.go ├── application.cpp ├── application.go ├── application.h ├── build └── docker │ ├── installer-script.qs │ ├── linux-static.dockerfile │ ├── linux.dockerfile │ ├── qt-static.dockerfile │ ├── win32-static.dockerfile │ ├── win32.dockerfile │ ├── win64-static.dockerfile │ └── win64.dockerfile ├── cmd └── qamel │ ├── main.go │ └── version.go ├── engine.cpp ├── engine.go ├── engine.h ├── examples ├── hello-world │ ├── main.go │ ├── res │ │ └── main.qml │ └── screenshot.png ├── live-reload │ ├── main.go │ ├── res │ │ └── main.qml │ └── screencast.gif ├── qml-go │ ├── backend.go │ ├── main.go │ ├── res │ │ └── main.qml │ └── screencast.gif └── qml-goroutine │ ├── backend.go │ ├── main.go │ ├── res │ └── main.qml │ └── screencast.gif ├── go.mod ├── go.sum ├── internal ├── cmd │ ├── build.go │ ├── docker.go │ ├── profile-delete.go │ ├── profile-list.go │ ├── profile-print.go │ ├── profile-setup.go │ ├── profile.go │ ├── root.go │ └── utils.go ├── config │ └── profile.go └── generator │ ├── cgo.go │ ├── deps-linux.go │ ├── deps-qml.go │ ├── deps-windows.go │ ├── deps.go │ ├── icon.go │ ├── moc.go │ ├── object-file.go │ ├── object-type.go │ ├── object.go │ ├── rcc.go │ └── utils.go ├── listmodel.cpp ├── listmodel.go ├── listmodel.h ├── object.go ├── quickstyle.cpp ├── quickstyle.go ├── quickstyle.h ├── tablemodel.cpp ├── tablemodel.go ├── tablemodel.h ├── viewer-enum.go ├── viewer.cpp ├── viewer.go └── viewer.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude config file 2 | .vscode/ 3 | .idea/ 4 | 5 | # Exclude generated file 6 | /qamel 7 | /moc-*.h -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Radhi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Qamel 2 | ----- 3 | 4 | [![GoDoc](https://godoc.org/github.com/go-qamel/qamel?status.png)](https://godoc.org/github.com/go-qamel/qamel) 5 | [![Docker](https://img.shields.io/badge/docker-qamel-blue.svg)](https://hub.docker.com/r/radhifadlillah/qamel) 6 | [![Donate](https://img.shields.io/badge/donate-PayPal-green.svg)](https://www.paypal.me/RadhiFadlillah) 7 | [![Donate](https://img.shields.io/badge/donate-Ko--fi-brightgreen)](https://ko-fi.com/radhifadlillah) 8 | 9 | Qamel is a simple QML binding for Go, heavily inspired by [`therecipe/qt`](https://github.com/therecipe/qt). This package only binds Qt's classes that used for creating a simple QML app, i.e. `QApplication`, `QQuickView` and `QQMLApplicationEngine`. It's still work-in progress, however it should be stable enough to use in production (as in I'm already using it in prod without problem, your situations may vary). 10 | 11 | ### Features 12 | 13 | - Published under MIT License, which means you can use this binding for whatever you want. 14 | - Since it only binds the small set of Qt's class, the build time is quite fast. 15 | - It's available as Docker image, which means you can create QML app without installing Qt in your PC. Go is still needed though. 16 | - The binding itself is really simple and small. I also think I did a good job on commenting my code, so people should be able to fork it easily. 17 | - It supports [live reload](https://godoc.org/github.com/go-qamel/qamel#Viewer.WatchResourceDir) which is really useful while working on GUI. 18 | 19 | ### Limitation 20 | 21 | - I've only tested this in Linux and Windows, so I'm not sure about Mac OS. It should works though, since the code itself is really simple. 22 | - When declaring custom QML object, this binding only [supports](https://github.com/go-qamel/qamel/wiki/QmlObject-Documentation) basic data type, i.e. `int`, `int32`, `int64`, `float32`, `float64`, `bool` and `string`. For other data type like struct, array or map, you have to use `string` type and pass it as JSON value. 23 | - Thanks to Go and Qt, in theory, the app built using this binding can be cross compiled from and to Windows, Linux and MacOS. However, since I only have Linux and Windows PC, I only able to test cross compiling between Linux and Windows. 24 | 25 | ### Development Status 26 | 27 | I've created this binding for my job, so it's actively maintained. However, since I created it for the sake of the job, if the issues are not critical and doesn't affect my job or workflow, it might take a long time before I work on it. Therefore, all PRs and contributors will always be welcomed. 28 | 29 | ### Resources 30 | 31 | All documentation for this binding is available in [wiki](https://github.com/go-qamel/qamel/wiki) and [GoDoc](https://godoc.org/github.com/go-qamel/qamel). There are some important sections in wiki that I recommend you to check before you start developing your QML app : 32 | 33 | - [Frequently Asked Questions](https://github.com/go-qamel/qamel/wiki/Frequently-Asked-Questions-(FAQ)), especially the [comparison](https://github.com/go-qamel/qamel/wiki/Frequently-Asked-Questions-(FAQ)#how-does-it-compare-to-therecipeqt-) with other binding. 34 | - [Getting Started](https://github.com/go-qamel/qamel/wiki/Getting-Started) 35 | - [CLI Usage](https://github.com/go-qamel/qamel/wiki/CLI-Usage) 36 | - [Building App](https://github.com/go-qamel/qamel/wiki/Building-Application) 37 | 38 | You might also want to check Qt's official documentation about QML : 39 | 40 | - [Qt QML](http://doc.qt.io/qt-5/qtqml-index.html) 41 | - [All QML Types](http://doc.qt.io/qt-5/qmltypes.html) 42 | - [QML Reference](http://doc.qt.io/qt-5/qmlreference.html) 43 | - [QML Code Conventions](https://doc.qt.io/qt-5/qml-codingconventions.html) 44 | 45 | For demo, you can check out [Qamel-HN](https://github.com/go-qamel/qamel-hn), a HackerNews reader built with QML and Go. 46 | 47 | ### Licenses 48 | 49 | Qamel is distributed under [MIT license](https://choosealicense.com/licenses/mit/), which means you can use and modify it however you want. However, if you make an enhancement for it, if possible, please send a pull request. If you like this project, please consider donating to me either via [PayPal](https://www.paypal.me/RadhiFadlillah) or [Ko-Fi](https://ko-fi.com/radhifadlillah). 50 | -------------------------------------------------------------------------------- /application-enum.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // FontWeight is the weight for thickness of the font. 4 | // Qt uses a weighting scale from 0 to 99 similar to, 5 | // but not the same as, the scales used in Windows or CSS. 6 | // A weight of 0 will be thin, whilst 99 will be extremely black. 7 | type FontWeight int32 8 | 9 | const ( 10 | // Thin is barely visible, usually called hairline 11 | Thin FontWeight = 0 12 | // ExtraLight is same with Ultra Light in CSS 13 | ExtraLight = 12 14 | // Light is quite visible and almost normal 15 | Light = 25 16 | // Normal is the normal font weight 17 | Normal = 50 18 | // Medium is a bit heavier than normal 19 | Medium = 57 20 | // DemiBold is similar with Semi Bold in CSS 21 | DemiBold = 63 22 | // Bold is the bold font, usually used for emphasis 23 | Bold = 75 24 | // ExtraBold is usually called Heavy, useful for title 25 | ExtraBold = 81 26 | // Black is the thickest font weight, usually used for poster 27 | Black = 87 28 | ) 29 | 30 | // Attribute describes attributes that change the behavior of application-wide features. 31 | type Attribute int64 32 | 33 | const ( 34 | // DontShowIconsInMenus make actions with the Icon property 35 | // won't be shown in any menus unless specifically set by 36 | // the QAction::iconVisibleInMenu property. 37 | // Menus that are currently open or menus already created in 38 | // the native macOS menubar may not pick up a change in this 39 | // attribute. Changes in the QAction::iconVisibleInMenu property 40 | // will always be picked up. 41 | DontShowIconsInMenus Attribute = 2 42 | 43 | // DontShowShortcutsInContextMenus make actions with the Shortcut 44 | // property won't be shown in any shortcut menus unless specifically 45 | // set by the QAction::shortcutVisibleInContextMenu property. 46 | // This value was added in Qt 5.10. 47 | DontShowShortcutsInContextMenus = 28 48 | 49 | // NativeWindows ensures that widgets have native windows. 50 | NativeWindows = 3 51 | 52 | // DontCreateNativeWidgetSiblings ensures that siblings of native 53 | // widgets stay non-native unless specifically set by the 54 | // Qt::WA_NativeWindow attribute. 55 | DontCreateNativeWidgetSiblings = 4 56 | 57 | // PluginApplication indicates that Qt is used to author a plugin. 58 | // Depending on the operating system, it suppresses specific 59 | // initializations that do not necessarily make sense in the plugin case. 60 | // For example on macOS, this includes avoiding loading our nib for the 61 | // main menu and not taking possession of the native menu bar. Setting 62 | // this attribute to true will also set the AA_DontUseNativeMenuBar 63 | // attribute to true. It also disables native event filters. This 64 | // attribute must be set before QGuiApplication constructed. 65 | // This value was added in Qt 5.7. 66 | PluginApplication = 5 67 | 68 | // DontUseNativeMenuBar make all menubars created while this attribute 69 | // is set to true won't be used as a native menubar (e.g, the menubar 70 | // at the top of the main screen on macOS). 71 | DontUseNativeMenuBar = 6 72 | 73 | // MacDontSwapCtrlAndMeta prevents Qt swaps the Control and Meta (Command) 74 | // keys in MacOS. Whenever Control is pressed, Qt sends Meta, and whenever 75 | // Meta is pressed Control is sent. When this attribute is true, Qt will 76 | // not do the flip. QKeySequence::StandardKey will also flip accordingly 77 | // (i.e., QKeySequence::Copy will be Command+C on the keyboard regardless 78 | // of the value set, though what is output for QKeySequence::toString() 79 | // will be different). 80 | MacDontSwapCtrlAndMeta = 7 81 | 82 | // Use96Dpi assumes the screen has a resolution of 96 DPI rather than 83 | // using the OS-provided resolution. This will cause font rendering to be 84 | // consistent in pixels-per-point across devices rather than defining 85 | // 1 point as 1/72 inch. 86 | Use96Dpi = 8 87 | 88 | // SynthesizeTouchForUnhandledMouseEvents make all mouse events that are 89 | // not accepted by the application will be translated to touch events instead. 90 | SynthesizeTouchForUnhandledMouseEvents = 11 91 | 92 | // SynthesizeMouseForUnhandledTouchEvents make all touch events that are 93 | // not accepted by the application will be translated to left button mouse 94 | // events instead. This attribute is enabled by default. 95 | SynthesizeMouseForUnhandledTouchEvents = 12 96 | 97 | // UseHighDpiPixmaps make QIcon::pixmap() generate high-dpi pixmaps that 98 | // can be larger than the requested size. Such pixmaps will have 99 | // devicePixelRatio() set to a value higher than 1. After setting this 100 | // attribute, application code that uses pixmap sizes in layout geometry 101 | // calculations should typically divide by devicePixelRatio() to get 102 | // device-independent layout geometry. 103 | UseHighDpiPixmaps = 13 104 | 105 | // ForceRasterWidgets make top-level widgets use pure raster surfaces, 106 | // and do not support non-native GL-based child widgets. 107 | ForceRasterWidgets = 14 108 | 109 | // UseDesktopOpenGL forces the usage of desktop OpenGL (for example, 110 | // opengl32.dll or libGL.so) on platforms that use dynamic loading of 111 | // the OpenGL implementation. This attribute must be set before 112 | // QGuiApplication is constructed. This value was added in Qt 5.3. 113 | UseDesktopOpenGL = 15 114 | 115 | // UseOpenGLES forces the usage of OpenGL ES 2.0 or higher on platforms 116 | // that use dynamic loading of the OpenGL implementation. This attribute 117 | // must be set before QGuiApplication is constructed. 118 | // This value was added in Qt 5.3. 119 | UseOpenGLES = 16 120 | 121 | // UseSoftwareOpenGL forces the usage of a software based OpenGL 122 | // implementation on platforms that use dynamic loading of the OpenGL 123 | // implementation. This will typically be a patched build of Mesa llvmpipe, 124 | // providing OpenGL 2.1. The value may have no effect if no such OpenGL 125 | // implementation is available. The default name of this library is 126 | // opengl32sw.dll and can be overridden by setting the environment variable 127 | // QT_OPENGL_DLL. See the platform-specific pages, for instance Qt for 128 | // Windows, for more information. This attribute must be set before 129 | // QGuiApplication is constructed. This value was added in Qt 5.4. 130 | UseSoftwareOpenGL = 17 131 | 132 | // ShareOpenGLContexts enables resource sharing between the OpenGL contexts 133 | // used by classes like QOpenGLWidget and QQuickWidget. This allows sharing 134 | // OpenGL resources, like textures, between QOpenGLWidget instances that 135 | // belong to different top-level windows. This attribute must be set 136 | // before QGuiApplication is constructed. This value was added in Qt 5.4. 137 | ShareOpenGLContexts = 18 138 | 139 | // SetPalette indicates whether a palette was explicitly set on the 140 | // QGuiApplication. This value was added in Qt 5.5. 141 | SetPalette = 19 142 | 143 | // EnableHighDpiScaling enables high-DPI scaling in Qt on supported platforms 144 | // (see also High DPI Displays). Supported platforms are X11, Windows and 145 | // Android. Enabling makes Qt scale the main (device independent) coordinate 146 | // system according to display scale factors provided by the operating system. 147 | // This corresponds to setting the QT_AUTO_SCREEN​_SCALE_FACTOR environment 148 | // variable to 1. This attribute must be set before QGuiApplication is 149 | // constructed. This value was added in Qt 5.6. 150 | EnableHighDpiScaling = 20 151 | 152 | // DisableHighDpiScaling disables high-DPI scaling in Qt, exposing window 153 | // system coordinates. Note that the window system may do its own scaling, 154 | // so this does not guarantee that QPaintDevice::devicePixelRatio() will be 155 | // equal to 1. In addition, scale factors set by QT_SCALE_FACTOR will not be 156 | // affected. This corresponds to setting the QT_AUTO_SCREEN​_SCALE_FACTOR 157 | // environment variable to 0. This attribute must be set before QGuiApplication 158 | // is constructed. This value was added in Qt 5.6. 159 | DisableHighDpiScaling = 21 160 | 161 | // UseStyleSheetPropagationInWidgetStyles enables Qt Style Sheet in regular QWidget. 162 | // By default, Qt Style Sheets disable regular QWidget palette and font propagation. 163 | // When this flag is enabled, font and palette changes propagate as though the 164 | // user had manually called the corresponding QWidget methods. See The Style 165 | // Sheet Syntax - Inheritance for more details. This value was added in Qt 5.7. 166 | UseStyleSheetPropagationInWidgetStyles = 22 167 | 168 | // DontUseNativeDialogs makes all dialogs created while this attribute is 169 | // set to true won't use the native dialogs provided by the platform. 170 | // This value was added in Qt 5.7. 171 | DontUseNativeDialogs = 23 172 | 173 | // SynthesizeMouseForUnhandledTabletEvents makes all tablet events that are not 174 | // accepted by the application will be translated to mouse events instead. 175 | // This attribute is enabled by default. This value was added in Qt 5.7. 176 | SynthesizeMouseForUnhandledTabletEvents = 24 177 | 178 | // CompressHighFrequencyEvents enables compression of certain frequent events. 179 | // On the X11 windowing system, the default value is true, which means that 180 | // QEvent::MouseMove, QEvent::TouchUpdate, and changes in window size and position 181 | // will be combined whenever they occur more frequently than the application 182 | // handles them, so that they don't accumulate and overwhelm the application later. 183 | // On other platforms, the default is false. (In the future, the compression 184 | // feature may be implemented across platforms.) You can test the attribute to 185 | // see whether compression is enabled. If your application needs to handle all 186 | // events with no compression, you can unset this attribute. Notice that input 187 | // events from tablet devices will not be compressed. See AA_CompressTabletEvents 188 | // if you want these to be compressed as well. This value was added in Qt 5.7. 189 | CompressHighFrequencyEvents = 25 190 | 191 | // CompressTabletEvents enables compression of input events from tablet devices. 192 | // Notice that AA_CompressHighFrequencyEvents must be true for events compression 193 | // to be enabled, and that this flag extends the former to tablet events. 194 | // Its default value is false. This value was added in Qt 5.10. 195 | CompressTabletEvents = 29 196 | 197 | // DontCheckOpenGLContextThreadAffinity makes a context that created using 198 | // QOpenGLContext does not check that the QObject thread affinity of the 199 | // QOpenGLContext object is the same thread calling makeCurrent(). 200 | // This value was added in Qt 5.8. 201 | DontCheckOpenGLContextThreadAffinity = 26 202 | 203 | // DisableShaderDiskCache disables caching of shader program binaries on disk. 204 | // By default Qt Quick, QPainter's OpenGL backend, and any application using 205 | // QOpenGLShaderProgram with one of its addCacheableShaderFromSource overloads 206 | // will employ a disk-based program binary cache in either the shared or 207 | // per-process cache storage location, on systems that support glProgramBinary(). 208 | // In the unlikely event of this being problematic, set this attribute to 209 | // disable all disk-based caching of shaders. 210 | DisableShaderDiskCache = 27 211 | 212 | // DisableWindowContextHelpButton disables the WindowContextHelpButtonHint by 213 | // default on Qt::Sheet and Qt::Dialog widgets. This hides the ? button on 214 | // Windows, which only makes sense if you use QWhatsThis functionality. 215 | // This value was added in Qt 5.10. In Qt 6, WindowContextHelpButtonHint 216 | // will not be set by default. 217 | DisableWindowContextHelpButton = 30 218 | ) 219 | -------------------------------------------------------------------------------- /application.cpp: -------------------------------------------------------------------------------- 1 | #include "application.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void* App_NewApplication(int argc, char* argv) { 10 | static int argcs = argc; 11 | static char** argvs = static_cast(malloc(argcs * sizeof(char*))); 12 | 13 | QList aList = QByteArray(argv).split('|'); 14 | for (int i = 0; i < argcs; i++) { 15 | argvs[i] = (new QByteArray(aList.at(i)))->data(); 16 | } 17 | 18 | QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 19 | return new QApplication(argcs, argvs); 20 | } 21 | 22 | void App_SetAttribute(long long attribute, bool on) { 23 | QApplication::setAttribute(Qt::ApplicationAttribute(attribute), on); 24 | } 25 | 26 | void App_SetFont(char *family, int pointSize, int weight, bool italic) { 27 | QFont font = QFont(QString(family), pointSize, weight, italic); 28 | QApplication::setFont(font); 29 | } 30 | 31 | void App_SetQuitOnLastWindowClosed(bool quit) { 32 | QApplication::setQuitOnLastWindowClosed(quit); 33 | } 34 | 35 | void App_SetApplicationDisplayName(char* name) { 36 | QApplication::setApplicationDisplayName(QString(name)); 37 | } 38 | 39 | void App_SetWindowIcon(char* fileName) { 40 | QIcon icon = QIcon(QString(fileName)); 41 | QApplication::setWindowIcon(icon); 42 | } 43 | 44 | void App_SetApplicationName(char* name) { 45 | QApplication::setApplicationName(QString(name)); 46 | } 47 | 48 | void App_SetApplicationVersion(char* version) { 49 | QApplication::setApplicationVersion(QString(version)); 50 | } 51 | 52 | void App_SetOrganizationName(char* name) { 53 | QApplication::setOrganizationName(QString(name)); 54 | } 55 | 56 | void App_SetOrganizationDomain(char* domain) { 57 | QApplication::setOrganizationDomain(QString(domain)); 58 | } 59 | 60 | int App_Exec() { 61 | return QApplication::exec(); 62 | } -------------------------------------------------------------------------------- /application.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include 7 | // #include "application.h" 8 | import "C" 9 | import ( 10 | "runtime" 11 | "strings" 12 | "unsafe" 13 | ) 14 | 15 | func init() { 16 | runtime.LockOSThread() 17 | } 18 | 19 | // Application is the main app which wraps QGuiApplication 20 | type Application struct { 21 | ptr unsafe.Pointer 22 | } 23 | 24 | // NewApplication initializes the window system and constructs 25 | // an QGuiApplication object with argc command line arguments in argv. 26 | func NewApplication(argc int, argv []string) *Application { 27 | argvC := C.CString(strings.Join(argv, "|")) 28 | defer C.free(unsafe.Pointer(argvC)) 29 | 30 | ptr := C.App_NewApplication(C.int(int32(argc)), argvC) 31 | return &Application{ptr: ptr} 32 | } 33 | 34 | // SetAttribute sets the Application's attribute if on is true; 35 | // otherwise clears the attribute 36 | func SetAttribute(attribute Attribute, on bool) { 37 | C.App_SetAttribute(C.longlong(attribute), C.bool(on)) 38 | } 39 | 40 | // SetFont changes the default application font 41 | func (app Application) SetFont(fontFamily string, pointSize int, weight FontWeight, italic bool) { 42 | cFontFamily := C.CString(fontFamily) 43 | defer C.free(unsafe.Pointer(cFontFamily)) 44 | C.App_SetFont(cFontFamily, C.int(int32(pointSize)), C.int(weight), C.bool(italic)) 45 | } 46 | 47 | // SetQuitOnLastWindowClosed set whether the application implicitly quits 48 | // when the last window is closed. The default is true. 49 | func (app Application) SetQuitOnLastWindowClosed(quit bool) { 50 | C.App_SetQuitOnLastWindowClosed(C.bool(quit)) 51 | } 52 | 53 | // SetApplicationDisplayName sets the user visible name of this application 54 | func (app Application) SetApplicationDisplayName(name string) { 55 | cName := C.CString(name) 56 | defer C.free(unsafe.Pointer(cName)) 57 | C.App_SetApplicationDisplayName(cName) 58 | } 59 | 60 | // SetWindowIcon sets the default window icon from the specified fileName. 61 | // The fileName could be a relative path (../icon), Qt resource path (qrc://icon), 62 | // or absolute file path (file://path/to/icon) 63 | func (app Application) SetWindowIcon(fileName string) { 64 | cFileName := C.CString(fileName) 65 | defer C.free(unsafe.Pointer(cFileName)) 66 | C.App_SetWindowIcon(cFileName) 67 | } 68 | 69 | // SetApplicationName sets the name of this application 70 | func (app Application) SetApplicationName(name string) { 71 | cName := C.CString(name) 72 | defer C.free(unsafe.Pointer(cName)) 73 | C.App_SetApplicationName(cName) 74 | } 75 | 76 | // SetApplicationVersion sets the version of this application 77 | func (app Application) SetApplicationVersion(version string) { 78 | cVersion := C.CString(version) 79 | defer C.free(unsafe.Pointer(cVersion)) 80 | C.App_SetApplicationVersion(cVersion) 81 | } 82 | 83 | // SetOrganizationName sets the name of the organization that 84 | // write this application 85 | func (app Application) SetOrganizationName(name string) { 86 | cName := C.CString(name) 87 | defer C.free(unsafe.Pointer(cName)) 88 | C.App_SetOrganizationName(cName) 89 | } 90 | 91 | // SetOrganizationDomain sets the internet domain of the 92 | // organization that write this application 93 | func (app Application) SetOrganizationDomain(domain string) { 94 | cDomain := C.CString(domain) 95 | defer C.free(unsafe.Pointer(cDomain)) 96 | C.App_SetOrganizationDomain(cDomain) 97 | } 98 | 99 | // Exec enters the main event loop and waits until exit() is called, and 100 | // then returns the value that was set to exit() (which is 0 if exit() is 101 | // called via quit()). 102 | func (app Application) Exec() int { 103 | return int(int32(C.App_Exec())) 104 | } 105 | -------------------------------------------------------------------------------- /application.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef QAMEL_APPLICATION_H 4 | #define QAMEL_APPLICATION_H 5 | 6 | #include 7 | #include 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | // Constructor 14 | void* App_NewApplication(int argc, char* argv); 15 | 16 | // Static function 17 | void App_SetAttribute(long long attribute, bool on); 18 | 19 | // Method 20 | void App_SetFont(char *family, int pointSize, int weight, bool italic); 21 | void App_SetQuitOnLastWindowClosed(bool quit); 22 | void App_SetApplicationDisplayName(char* name); 23 | void App_SetWindowIcon(char* fileName); 24 | void App_SetApplicationName(char* name); 25 | void App_SetApplicationVersion(char* version); 26 | void App_SetOrganizationName(char* name); 27 | void App_SetOrganizationDomain(char* domain); 28 | int App_Exec(); 29 | 30 | #ifdef __cplusplus 31 | } 32 | #endif 33 | 34 | #endif -------------------------------------------------------------------------------- /build/docker/installer-script.qs: -------------------------------------------------------------------------------- 1 | function Controller() { 2 | installer.autoRejectMessageBoxes(); 3 | } 4 | 5 | Controller.prototype.WelcomePageCallback = function() { 6 | // click delay here because the next button is initially disabled for ~1 second 7 | gui.clickButton(buttons.NextButton, 3000); 8 | } 9 | 10 | Controller.prototype.CredentialsPageCallback = function() { 11 | gui.clickButton(buttons.NextButton); 12 | } 13 | 14 | Controller.prototype.IntroductionPageCallback = function() { 15 | gui.clickButton(buttons.NextButton); 16 | } 17 | 18 | Controller.prototype.TargetDirectoryPageCallback = function() { 19 | gui.clickButton(buttons.NextButton); 20 | } 21 | 22 | Controller.prototype.ComponentSelectionPageCallback = function() { 23 | var qtVersion = 'qt5.5132', 24 | widget = gui.currentPageWidget(); 25 | 26 | widget.deselectAll(); 27 | widget.selectComponent('qt.'+qtVersion+'.gcc_64'); 28 | widget.selectComponent('qt.'+qtVersion+'.qtcharts'); 29 | widget.selectComponent('qt.'+qtVersion+'.qtdatavis3d'); 30 | gui.clickButton(buttons.NextButton); 31 | } 32 | 33 | Controller.prototype.LicenseAgreementPageCallback = function() { 34 | gui.currentPageWidget().AcceptLicenseRadioButton.setChecked(true); 35 | gui.clickButton(buttons.NextButton); 36 | } 37 | 38 | Controller.prototype.StartMenuDirectoryPageCallback = function() { 39 | gui.clickButton(buttons.NextButton); 40 | } 41 | 42 | Controller.prototype.ReadyForInstallationPageCallback = function() { 43 | gui.clickButton(buttons.NextButton); 44 | } 45 | 46 | Controller.prototype.PerformInstallationPageCallback = function() { 47 | gui.currentPageWidget().setAutomatedPageSwitchEnabled(true); 48 | } 49 | 50 | Controller.prototype.FinishedPageCallback = function() { 51 | var checkBoxForm = gui.currentPageWidget().LaunchQtCreatorCheckBoxForm; 52 | if (checkBoxForm && checkBoxForm.launchQtCreatorCheckBox) { 53 | checkBoxForm.launchQtCreatorCheckBox.checked = false; 54 | } 55 | 56 | gui.clickButton(buttons.FinishButton); 57 | } -------------------------------------------------------------------------------- /build/docker/linux-static.dockerfile: -------------------------------------------------------------------------------- 1 | FROM radhifadlillah/qamel:linux as linux 2 | 3 | # ========== END OF LINUX ========== # 4 | 5 | FROM radhifadlillah/qamel:qt-static as base 6 | 7 | # ========== END OF BASE ========== # 8 | 9 | FROM ubuntu:16.04 10 | 11 | ENV HOME /home/user 12 | ENV GOPATH $HOME/go 13 | ENV QT_VERSION 5.13.2 14 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 15 | 16 | # Install ca-certificates which might be needed by Go proxy 17 | RUN apt-get -qq update && \ 18 | apt-get -qq -y install ca-certificates git 19 | 20 | # Install dependencies for Qt5 21 | RUN apt-get -qq update && \ 22 | apt-get -qq -y install python libgl1-mesa-dev \ 23 | libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev \ 24 | libxfixes-dev libxi-dev libxrender-dev libxcb1-dev \ 25 | libx11-xcb-dev libxcb-glx0-dev libxkbcommon-dev \ 26 | libxkbcommon-x11-dev '^libxcb.*-dev' && \ 27 | apt-get -qq clean 28 | 29 | # Install ccache for faster build 30 | RUN apt-get -qq update && \ 31 | apt-get -qq -y install ccache && \ 32 | apt-get -qq clean 33 | ENV PATH "/usr/lib/ccache:$PATH" 34 | 35 | # Copy Qt5 from base 36 | COPY --from=base /opt/Qt$QT_VERSION /opt/Qt$QT_VERSION 37 | 38 | # Copy Go and Qamel from linux 39 | COPY --from=linux /usr/local/go /usr/local/go 40 | COPY --from=linux $GOPATH/bin $GOPATH/bin 41 | COPY --from=linux $GOPATH/src/github.com/go-qamel/qamel $GOPATH/src/github.com/go-qamel/qamel 42 | 43 | # Create profile for Qamel 44 | RUN mkdir -p $HOME/.config/qamel 45 | RUN printf '%s %s %s %s %s %s %s %s %s %s\n' \ 46 | '{"default": {' \ 47 | '"OS":"linux",' \ 48 | '"Arch":"amd64",' \ 49 | '"Static":true,' \ 50 | '"Qmake":"/opt/Qt5.13.2/bin/qmake",' \ 51 | '"Moc":"/opt/Qt5.13.2/bin/moc",' \ 52 | '"Rcc":"/opt/Qt5.13.2/bin/rcc",' \ 53 | '"Gcc":"gcc",' \ 54 | '"Gxx":"g++"' \ 55 | '}}' > $HOME/.config/qamel/config.json 56 | 57 | # Build app 58 | ENTRYPOINT ["qamel", "build"] -------------------------------------------------------------------------------- /build/docker/linux.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 as base 2 | 3 | ENV HOME /home/user 4 | ENV GOPATH $HOME/go 5 | ENV GO_VERSION 1.13.4 6 | ENV QT_MAJOR 5.13 7 | ENV QT_VERSION 5.13.2 8 | 9 | # Install Go 10 | RUN apt-get -qq update && \ 11 | apt-get -qq -y install ca-certificates curl git 12 | RUN curl -SL --retry 10 --retry-delay 60 https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz | \ 13 | tar -xzC /usr/local 14 | 15 | # Install Qt5 dependencies 16 | RUN apt-get -qq update && \ 17 | apt-get -qq -y install dbus libfontconfig1 libx11-6 libx11-xcb1 18 | 19 | # Download Qt5 20 | RUN curl -SL --retry 10 --retry-delay 60 -O \ 21 | https://download.qt.io/official_releases/qt/$QT_MAJOR/$QT_VERSION/qt-opensource-linux-x64-$QT_VERSION.run 22 | 23 | # Download Qt5 installation script 24 | RUN curl -SL --retry 10 --retry-delay 60 -O \ 25 | https://raw.githubusercontent.com/go-qamel/qamel/master/build/docker/installer-script.qs #0fabfd 26 | 27 | # Install Qt5 28 | RUN chmod +x qt-opensource-linux-x64-$QT_VERSION.run && \ 29 | ./qt-opensource-linux-x64-$QT_VERSION.run -v \ 30 | --script installer-script.qs \ 31 | --platform minimal 32 | 33 | # Clean up after installing Qt5 34 | RUN rm -Rf /opt/Qt$QT_VERSION/Docs \ 35 | /opt/Qt$QT_VERSION/Examples \ 36 | /opt/Qt$QT_VERSION/Tools 37 | 38 | # Install Qamel 39 | RUN /usr/local/go/bin/go get -u github.com/go-qamel/qamel/cmd/qamel #0fabfd 40 | 41 | # ========== END OF BASE ========== # 42 | 43 | FROM ubuntu:16.04 44 | 45 | ENV HOME /home/user 46 | ENV GOPATH $HOME/go 47 | ENV QT_VERSION 5.13.2 48 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 49 | 50 | # Install ca-certificates which might be needed by Go proxy 51 | RUN apt-get -qq update && \ 52 | apt-get -qq -y install ca-certificates git 53 | 54 | # Install dependencies for Qt5 55 | RUN apt-get -qq update && \ 56 | apt-get -qq -y install build-essential libgl1-mesa-dev \ 57 | libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev \ 58 | libxfixes-dev libxi-dev libxrender-dev libxcb1-dev \ 59 | libx11-xcb-dev libxcb-glx0-dev libxkbcommon-x11-dev && \ 60 | apt-get -qq clean 61 | 62 | # Install ccache for faster build 63 | RUN apt-get -qq update && \ 64 | apt-get -qq -y install ccache && \ 65 | apt-get -qq clean 66 | ENV PATH "/usr/lib/ccache:$PATH" 67 | 68 | # Copy Go and Qamel from base 69 | COPY --from=base /usr/local/go /usr/local/go 70 | COPY --from=base $GOPATH/bin $GOPATH/bin 71 | COPY --from=base $GOPATH/src/github.com/go-qamel/qamel $GOPATH/src/github.com/go-qamel/qamel 72 | 73 | # Copy Qt5 from base 74 | COPY --from=base /opt/Qt$QT_VERSION /opt/Qt$QT_VERSION 75 | 76 | # Create profile for Qamel 77 | RUN mkdir -p $HOME/.config/qamel 78 | RUN printf '%s %s %s %s %s %s %s %s %s %s\n' \ 79 | '{"default": {' \ 80 | '"OS":"linux",' \ 81 | '"Arch":"amd64",' \ 82 | '"Static":false,' \ 83 | '"Qmake":"/opt/Qt5.13.2/5.13.2/gcc_64/bin/qmake",' \ 84 | '"Moc":"/opt/Qt5.13.2/5.13.2/gcc_64/bin/moc",' \ 85 | '"Rcc":"/opt/Qt5.13.2/5.13.2/gcc_64/bin/rcc",' \ 86 | '"Gcc":"gcc",' \ 87 | '"Gxx":"g++"' \ 88 | '}}' > $HOME/.config/qamel/config.json 89 | 90 | # Build app 91 | ENTRYPOINT ["qamel", "build"] -------------------------------------------------------------------------------- /build/docker/qt-static.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 as base 2 | 3 | # Download source of Qt5 4 | RUN apt-get -qq update && \ 5 | apt-get -qq -y install curl build-essential 6 | 7 | ENV QT_MAJOR 5.13 8 | ENV QT_VERSION 5.13.2 9 | RUN curl -SL --retry 10 --retry-delay 60 https://download.qt.io/official_releases/qt/$QT_MAJOR/$QT_VERSION/single/qt-everywhere-src-$QT_VERSION.tar.xz | \ 10 | tar -xJC / 11 | 12 | # Install dependencies for Qt5 13 | RUN apt-get -qq update && \ 14 | apt-get -qq -y install python libgl1-mesa-dev \ 15 | libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev \ 16 | libxfixes-dev libxi-dev libxrender-dev libxcb1-dev \ 17 | libx11-xcb-dev libxcb-glx0-dev libxkbcommon-dev \ 18 | libxkbcommon-x11-dev '^libxcb.*-dev' 19 | 20 | # Build Qt5 static 21 | RUN cd qt-everywhere-src-$QT_VERSION && \ 22 | ./configure -static -prefix "/opt/Qt$QT_VERSION" \ 23 | -opensource -confirm-license -release \ 24 | -optimize-size -strip -fontconfig \ 25 | -qt-zlib -qt-libjpeg -qt-libpng -qt-xcb \ 26 | -qt-pcre -qt-harfbuzz -qt-doubleconversion \ 27 | -nomake tools -nomake examples -nomake tests \ 28 | -no-pch -skip qtwebengine && \ 29 | make -j $(grep -c ^processor /proc/cpuinfo) && \ 30 | make install -j $(grep -c ^processor /proc/cpuinfo) -------------------------------------------------------------------------------- /build/docker/win32-static.dockerfile: -------------------------------------------------------------------------------- 1 | FROM radhifadlillah/qamel:linux as linux 2 | 3 | # ========== END OF LINUX ========== # 4 | 5 | FROM ubuntu:16.04 as base 6 | 7 | RUN apt-get -qq update && \ 8 | apt-get -qq -y install software-properties-common apt-transport-https 9 | 10 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 11 | RUN add-apt-repository 'deb [arch=amd64] https://mirror.mxe.cc/repos/apt bionic main' 12 | RUN apt-get -qq update && \ 13 | apt-get -qq -y install mxe-i686-w64-mingw32.static-qt5 14 | 15 | # ========== END OF BASE ========== # 16 | 17 | FROM ubuntu:16.04 18 | 19 | ENV HOME /home/user 20 | ENV GOPATH $HOME/go 21 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 22 | 23 | # Install ca-certificates which might be needed by Go proxy 24 | RUN apt-get -qq update && \ 25 | apt-get -qq -y install ca-certificates git 26 | 27 | # Copy Go and Qamel from linux 28 | COPY --from=linux /usr/local/go /usr/local/go 29 | COPY --from=linux $GOPATH/bin $GOPATH/bin 30 | COPY --from=linux $GOPATH/src/github.com/go-qamel/qamel $GOPATH/src/github.com/go-qamel/qamel 31 | 32 | # Copy MXE from base 33 | COPY --from=base /usr/lib/mxe /usr/lib/mxe 34 | 35 | # Create profile for Qamel 36 | RUN mkdir -p $HOME/.config/qamel 37 | RUN printf '%s %s %s %s %s %s %s %s %s %s %s\n' \ 38 | '{"default": {' \ 39 | '"OS":"windows",' \ 40 | '"Arch":"386",' \ 41 | '"Static":true,' \ 42 | '"Qmake":"/usr/lib/mxe/usr/i686-w64-mingw32.static/qt5/bin/qmake",' \ 43 | '"Moc":"/usr/lib/mxe/usr/i686-w64-mingw32.static/qt5/bin/moc",' \ 44 | '"Rcc":"/usr/lib/mxe/usr/i686-w64-mingw32.static/qt5/bin/rcc",' \ 45 | '"Gcc":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-gcc",' \ 46 | '"Gxx":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-g++",' \ 47 | '"Windres":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-windres"' \ 48 | '}}' > $HOME/.config/qamel/config.json 49 | 50 | # Build app 51 | ENTRYPOINT ["qamel", "build"] -------------------------------------------------------------------------------- /build/docker/win32.dockerfile: -------------------------------------------------------------------------------- 1 | FROM radhifadlillah/qamel:linux as linux 2 | 3 | # ========== END OF LINUX ========== # 4 | 5 | FROM ubuntu:16.04 as base 6 | 7 | RUN apt-get -qq update && \ 8 | apt-get -qq -y install software-properties-common apt-transport-https 9 | 10 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 11 | RUN add-apt-repository 'deb [arch=amd64] https://mirror.mxe.cc/repos/apt bionic main' 12 | RUN apt-get -qq update && \ 13 | apt-get -qq -y install mxe-i686-w64-mingw32.shared-qt5 14 | 15 | # ========== END OF BASE ========== # 16 | 17 | FROM ubuntu:16.04 18 | 19 | ENV HOME /home/user 20 | ENV GOPATH $HOME/go 21 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 22 | 23 | # Install ca-certificates which might be needed by Go proxy 24 | RUN apt-get -qq update && \ 25 | apt-get -qq -y install ca-certificates git 26 | 27 | # Copy Go and Qamel from linux 28 | COPY --from=linux /usr/local/go /usr/local/go 29 | COPY --from=linux $GOPATH/bin $GOPATH/bin 30 | COPY --from=linux $GOPATH/src/github.com/go-qamel/qamel $GOPATH/src/github.com/go-qamel/qamel 31 | 32 | # Copy MXE from base 33 | COPY --from=base /usr/lib/mxe /usr/lib/mxe 34 | 35 | # Create profile for Qamel 36 | RUN mkdir -p $HOME/.config/qamel 37 | RUN printf '%s %s %s %s %s %s %s %s %s %s %s %s\n' \ 38 | '{"default":{' \ 39 | '"OS":"windows",' \ 40 | '"Arch":"386",' \ 41 | '"Static":false,' \ 42 | '"Qmake":"/usr/lib/mxe/usr/i686-w64-mingw32.shared/qt5/bin/qmake",' \ 43 | '"Moc":"/usr/lib/mxe/usr/i686-w64-mingw32.shared/qt5/bin/moc",' \ 44 | '"Rcc":"/usr/lib/mxe/usr/i686-w64-mingw32.shared/qt5/bin/rcc",' \ 45 | '"Gcc":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.shared-gcc",' \ 46 | '"Gxx":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.shared-g++",' \ 47 | '"Windres":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.shared-windres",' \ 48 | '"Objdump":"/usr/lib/mxe/usr/bin/i686-w64-mingw32.shared-objdump"' \ 49 | '}}' > $HOME/.config/qamel/config.json 50 | 51 | # Build app 52 | ENTRYPOINT ["qamel", "build"] -------------------------------------------------------------------------------- /build/docker/win64-static.dockerfile: -------------------------------------------------------------------------------- 1 | FROM radhifadlillah/qamel:linux as linux 2 | 3 | # ========== END OF LINUX ========== # 4 | 5 | FROM ubuntu:16.04 as base 6 | 7 | RUN apt-get -qq update && \ 8 | apt-get -qq -y install software-properties-common apt-transport-https 9 | 10 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 11 | RUN add-apt-repository 'deb [arch=amd64] https://pkg.mxe.cc/repos/apt bionic main' 12 | RUN apt-get -qq update && \ 13 | apt-get -qq -y install mxe-x86-64-w64-mingw32.static-qt5 14 | 15 | # ========== END OF BASE ========== # 16 | 17 | FROM ubuntu:16.04 18 | 19 | ENV HOME /home/user 20 | ENV GOPATH $HOME/go 21 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 22 | 23 | # Install ca-certificates which might be needed by Go proxy 24 | RUN apt-get -qq update && \ 25 | apt-get -qq -y install ca-certificates git 26 | 27 | # Copy Go and Qamel from linux 28 | COPY --from=linux /usr/local/go /usr/local/go 29 | COPY --from=linux $GOPATH/bin $GOPATH/bin 30 | COPY --from=linux $GOPATH/src/github.com/go-qamel/qamel $GOPATH/src/github.com/go-qamel/qamel 31 | 32 | # Copy MXE from base 33 | COPY --from=base /usr/lib/mxe /usr/lib/mxe 34 | 35 | # Create profile for Qamel 36 | RUN mkdir -p $HOME/.config/qamel 37 | RUN printf '%s %s %s %s %s %s %s %s %s %s %s\n' \ 38 | '{"default": {' \ 39 | '"OS":"windows",' \ 40 | '"Arch":"amd64",' \ 41 | '"Static":true,' \ 42 | '"Qmake":"/usr/lib/mxe/usr/x86_64-w64-mingw32.static/qt5/bin/qmake",' \ 43 | '"Moc":"/usr/lib/mxe/usr/x86_64-w64-mingw32.static/qt5/bin/moc",' \ 44 | '"Rcc":"/usr/lib/mxe/usr/x86_64-w64-mingw32.static/qt5/bin/rcc",' \ 45 | '"Gcc":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.static-gcc",' \ 46 | '"Gxx":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.static-g++",' \ 47 | '"Windres":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.static-windres"' \ 48 | '}}' > $HOME/.config/qamel/config.json 49 | 50 | # Build app 51 | ENTRYPOINT ["qamel", "build"] -------------------------------------------------------------------------------- /build/docker/win64.dockerfile: -------------------------------------------------------------------------------- 1 | FROM radhifadlillah/qamel:linux as linux 2 | 3 | # ========== END OF LINUX ========== # 4 | 5 | FROM ubuntu:16.04 as base 6 | 7 | RUN apt-get -qq update && \ 8 | apt-get -qq -y install software-properties-common apt-transport-https 9 | 10 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 11 | RUN add-apt-repository 'deb [arch=amd64] https://pkg.mxe.cc/repos/apt bionic main' 12 | RUN apt-get -qq update && \ 13 | apt-get -qq -y install mxe-x86-64-w64-mingw32.shared-qt5 14 | 15 | # ========== END OF BASE ========== # 16 | 17 | FROM ubuntu:16.04 18 | 19 | ENV HOME /home/user 20 | ENV GOPATH $HOME/go 21 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 22 | 23 | # Install ca-certificates which might be needed by Go proxy 24 | RUN apt-get -qq update && \ 25 | apt-get -qq -y install ca-certificates git 26 | 27 | # Copy Go and Qamel from linux 28 | COPY --from=linux /usr/local/go /usr/local/go 29 | COPY --from=linux $GOPATH/bin $GOPATH/bin 30 | COPY --from=linux $GOPATH/src/github.com/go-qamel/qamel $GOPATH/src/github.com/go-qamel/qamel 31 | 32 | # Copy MXE from base 33 | COPY --from=base /usr/lib/mxe /usr/lib/mxe 34 | 35 | # Create profile for Qamel 36 | RUN mkdir -p $HOME/.config/qamel 37 | RUN printf '%s %s %s %s %s %s %s %s %s %s %s %s\n' \ 38 | '{"default":{' \ 39 | '"OS":"windows",' \ 40 | '"Arch":"amd64",' \ 41 | '"Static":false,' \ 42 | '"Qmake":"/usr/lib/mxe/usr/x86_64-w64-mingw32.shared/qt5/bin/qmake",' \ 43 | '"Moc":"/usr/lib/mxe/usr/x86_64-w64-mingw32.shared/qt5/bin/moc",' \ 44 | '"Rcc":"/usr/lib/mxe/usr/x86_64-w64-mingw32.shared/qt5/bin/rcc",' \ 45 | '"Gcc":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.shared-gcc",' \ 46 | '"Gxx":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.shared-g++",' \ 47 | '"Windres":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.shared-windres",' \ 48 | '"Objdump":"/usr/lib/mxe/usr/bin/x86_64-w64-mingw32.shared-objdump"' \ 49 | '}}' > $HOME/.config/qamel/config.json 50 | 51 | # Build app 52 | ENTRYPOINT ["qamel", "build"] -------------------------------------------------------------------------------- /cmd/qamel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-qamel/qamel/internal/cmd" 8 | ) 9 | 10 | func main() { 11 | rootCmd := cmd.QamelCmd() 12 | rootCmd.Version = version 13 | 14 | if err := rootCmd.Execute(); err != nil { 15 | fmt.Println(err) 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/qamel/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var version = "0fabfd65139f6dc7d04ed07f7f6b8563528e27b7" 4 | -------------------------------------------------------------------------------- /engine.cpp: -------------------------------------------------------------------------------- 1 | #include "engine.h" 2 | #include 3 | #include 4 | #include 5 | 6 | void* Engine_NewEngine() { 7 | return new QQmlApplicationEngine(); 8 | } 9 | 10 | void Engine_Load(void* ptr, char* url) { 11 | QQmlApplicationEngine *engine = static_cast(ptr); 12 | engine->load(QUrl(QString(url))); 13 | } 14 | 15 | void Engine_ClearComponentCache(void* ptr) { 16 | QQmlApplicationEngine *engine = static_cast(ptr); 17 | engine->clearComponentCache(); 18 | } 19 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include 7 | // #include "engine.h" 8 | import "C" 9 | import "unsafe" 10 | 11 | // Engine is the wrapper for QQMLApplicationEngine 12 | type Engine struct { 13 | ptr unsafe.Pointer 14 | } 15 | 16 | // NewEngine creates a new QQmlApplicationEngine with the given parent. 17 | // You will have to call load() later in order to load a QML file. 18 | func NewEngine() Engine { 19 | ptr := C.Engine_NewEngine() 20 | return Engine{ptr: ptr} 21 | } 22 | 23 | // NewEngineWithSource constructs a QQmlApplicationEngine with the given QML source. 24 | func NewEngineWithSource(source string) Engine { 25 | engine := NewEngine() 26 | engine.Load(source) 27 | return engine 28 | } 29 | 30 | // Load loads the root QML file located at url. The object tree defined by the file is 31 | // created immediately for local file urls. 32 | func (engine Engine) Load(url string) { 33 | if engine.ptr == nil { 34 | return 35 | } 36 | 37 | cURL := C.CString(url) 38 | defer C.free(unsafe.Pointer(cURL)) 39 | C.Engine_Load(engine.ptr, cURL) 40 | } 41 | 42 | // ClearComponentCache clears the engine's internal component cache. This function causes the property 43 | // metadata of all components previously loaded by the engine to be destroyed. All previously loaded 44 | // components and the property bindings for all extant objects created from those components will cease 45 | // to function. This function returns the engine to a state where it does not contain any loaded 46 | // component data. This may be useful in order to reload a smaller subset of the previous component set, 47 | // or to load a new version of a previously loaded component. Once the component cache has been cleared, 48 | // components must be loaded before any new objects can be created. 49 | func (engine Engine) ClearComponentCache() { 50 | if engine.ptr == nil { 51 | return 52 | } 53 | 54 | C.Engine_ClearComponentCache(engine.ptr) 55 | } 56 | -------------------------------------------------------------------------------- /engine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef QAMEL_ENGINE_H 4 | #define QAMEL_ENGINE_H 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | // Constructors 11 | void* Engine_NewEngine(); 12 | 13 | // Methods 14 | void Engine_Load(void* ptr, char* url); 15 | void Engine_ClearComponentCache(void* ptr); 16 | 17 | #ifdef __cplusplus 18 | } 19 | #endif 20 | 21 | #endif -------------------------------------------------------------------------------- /examples/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-qamel/qamel" 7 | ) 8 | 9 | func main() { 10 | // Create application 11 | app := qamel.NewApplication(len(os.Args), os.Args) 12 | app.SetApplicationDisplayName("Qamel Example") 13 | 14 | // Create a QML viewer 15 | view := qamel.NewViewerWithSource("qrc:/res/main.qml") 16 | view.SetResizeMode(qamel.SizeRootObjectToView) 17 | view.SetHeight(300) 18 | view.SetWidth(400) 19 | view.Show() 20 | 21 | // Exec app 22 | app.Exec() 23 | } 24 | -------------------------------------------------------------------------------- /examples/hello-world/res/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | 3 | Rectangle { 4 | color: "cyan" 5 | 6 | Text { 7 | anchors.fill: parent 8 | text: "Hello World" 9 | font.pixelSize: 32 10 | font.weight: Font.Bold 11 | verticalAlignment: Text.AlignVCenter 12 | horizontalAlignment: Text.AlignHCenter 13 | } 14 | } -------------------------------------------------------------------------------- /examples/hello-world/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-qamel/qamel/84f4b9917a91d7e3d12f4c99c5c2d463c0b85b35/examples/hello-world/screenshot.png -------------------------------------------------------------------------------- /examples/live-reload/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | fp "path/filepath" 7 | 8 | "github.com/go-qamel/qamel" 9 | ) 10 | 11 | func main() { 12 | // Create application 13 | app := qamel.NewApplication(len(os.Args), os.Args) 14 | app.SetApplicationDisplayName("Live Reload Example") 15 | 16 | // Create a QML viewer 17 | view := qamel.NewViewerWithSource("res/main.qml") 18 | view.SetResizeMode(qamel.SizeRootObjectToView) 19 | view.SetHeight(300) 20 | view.SetWidth(400) 21 | view.Show() 22 | 23 | // Watch change in resource dir 24 | projectDir, err := os.Getwd() 25 | if err != nil { 26 | log.Fatalln("Failed to get working directory:", err) 27 | } 28 | 29 | resDir := fp.Join(projectDir, "res") 30 | go view.WatchResourceDir(resDir) 31 | 32 | // Exec app 33 | app.Exec() 34 | } 35 | -------------------------------------------------------------------------------- /examples/live-reload/res/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | 3 | Rectangle { 4 | color: "cyan" 5 | 6 | Text { 7 | anchors.fill: parent 8 | text: "Hello World" 9 | font.pixelSize: 32 10 | font.weight: Font.Bold 11 | verticalAlignment: Text.AlignVCenter 12 | horizontalAlignment: Text.AlignHCenter 13 | } 14 | } -------------------------------------------------------------------------------- /examples/live-reload/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-qamel/qamel/84f4b9917a91d7e3d12f4c99c5c2d463c0b85b35/examples/live-reload/screencast.gif -------------------------------------------------------------------------------- /examples/qml-go/backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/go-qamel/qamel" 8 | ) 9 | 10 | // BackEnd is the bridge for communicating between QML and Go 11 | type BackEnd struct { 12 | qamel.QmlObject 13 | _ func() int `slot:"getRandomNumber"` 14 | } 15 | 16 | func (b *BackEnd) getRandomNumber() int { 17 | rand.Seed(time.Now().UTC().UnixNano()) 18 | return rand.Intn(9999) 19 | } 20 | -------------------------------------------------------------------------------- /examples/qml-go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-qamel/qamel" 7 | ) 8 | 9 | func init() { 10 | // Register the BackEnd as QML component 11 | RegisterQmlBackEnd("BackEnd", 1, 0, "BackEnd") 12 | } 13 | 14 | func main() { 15 | // Create application 16 | app := qamel.NewApplication(len(os.Args), os.Args) 17 | app.SetApplicationDisplayName("Qamel Example") 18 | 19 | // Create a QML viewer 20 | view := qamel.NewViewerWithSource("qrc:/res/main.qml") 21 | view.SetResizeMode(qamel.SizeRootObjectToView) 22 | view.SetHeight(300) 23 | view.SetWidth(400) 24 | view.Show() 25 | 26 | // Exec app 27 | app.Exec() 28 | } 29 | -------------------------------------------------------------------------------- /examples/qml-go/res/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Layouts 1.12 3 | import QtQuick.Controls 2.5 4 | import BackEnd 1.0 5 | 6 | Rectangle { 7 | color: "white" 8 | 9 | BackEnd { 10 | id: backEnd 11 | } 12 | 13 | ColumnLayout { 14 | spacing: 20 15 | anchors.verticalCenter: parent.verticalCenter 16 | anchors.horizontalCenter: parent.horizontalCenter 17 | Text { 18 | id: txt 19 | text: "Hello World" 20 | font.pixelSize: 32 21 | font.weight: Font.Bold 22 | horizontalAlignment: Text.AlignHCenter 23 | Layout.fillWidth: true 24 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter 25 | } 26 | 27 | Button { 28 | text: "Create Random Number" 29 | width: 300 30 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter 31 | onClicked: { 32 | let val = backEnd.getRandomNumber(); 33 | txt.text = val.toString(); 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /examples/qml-go/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-qamel/qamel/84f4b9917a91d7e3d12f4c99c5c2d463c0b85b35/examples/qml-go/screencast.gif -------------------------------------------------------------------------------- /examples/qml-goroutine/backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-qamel/qamel" 7 | ) 8 | 9 | // BackEnd is the bridge for communicating between QML and Go 10 | type BackEnd struct { 11 | qamel.QmlObject 12 | _ func() `constructor:"init"` 13 | _ func(string) `signal:"timeChanged"` 14 | } 15 | 16 | func (b *BackEnd) init() { 17 | go func() { 18 | for { 19 | now := time.Now().Format("15:04:05") 20 | b.timeChanged(now) 21 | time.Sleep(time.Second) 22 | } 23 | }() 24 | } 25 | -------------------------------------------------------------------------------- /examples/qml-goroutine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-qamel/qamel" 7 | ) 8 | 9 | func init() { 10 | // Register the BackEnd as QML component 11 | RegisterQmlBackEnd("BackEnd", 1, 0, "BackEnd") 12 | } 13 | 14 | func main() { 15 | // Create application 16 | app := qamel.NewApplication(len(os.Args), os.Args) 17 | app.SetApplicationDisplayName("Qamel Goroutine Example") 18 | 19 | // Create a QML viewer 20 | view := qamel.NewViewerWithSource("qrc:/res/main.qml") 21 | view.SetResizeMode(qamel.SizeRootObjectToView) 22 | view.SetHeight(300) 23 | view.SetWidth(400) 24 | view.Show() 25 | 26 | // Exec app 27 | app.Exec() 28 | } 29 | -------------------------------------------------------------------------------- /examples/qml-goroutine/res/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import BackEnd 1.0 3 | 4 | Rectangle { 5 | color: "white" 6 | 7 | BackEnd { 8 | id: backEnd 9 | onTimeChanged: (time) => txt.text = time 10 | } 11 | 12 | Text { 13 | id: txt 14 | anchors.fill: parent 15 | font.pixelSize: 32 16 | font.weight: Font.Bold 17 | verticalAlignment: Text.AlignVCenter 18 | horizontalAlignment: Text.AlignHCenter 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/qml-goroutine/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-qamel/qamel/84f4b9917a91d7e3d12f4c99c5c2d463c0b85b35/examples/qml-goroutine/screencast.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-qamel/qamel 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fatih/color v1.7.0 7 | github.com/fsnotify/fsnotify v1.4.7 8 | github.com/mattn/go-colorable v0.1.2 // indirect 9 | github.com/muesli/go-app-paths v0.2.1 10 | github.com/sirupsen/logrus v1.4.2 11 | github.com/spf13/cobra v0.0.5 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 10 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 11 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 14 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 15 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 16 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 17 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 18 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 19 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 20 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 21 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 22 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 23 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 24 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 25 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 26 | github.com/muesli/go-app-paths v0.0.0-20181030220709-913f7f7ac60f h1:qC86+y8MoTDwlkAeS4p8fuo9nzKtZV/Gg9Nbqeu1+LM= 27 | github.com/muesli/go-app-paths v0.0.0-20181030220709-913f7f7ac60f/go.mod h1:YIG7FlQLGglsbGA+CX6/boYl9aNdoQXfx+ZtACJCMug= 28 | github.com/muesli/go-app-paths v0.2.1/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho= 29 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 33 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 34 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 35 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 36 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 37 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 38 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 39 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 40 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 41 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 42 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 43 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 45 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 46 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 47 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 48 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 49 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 51 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 52 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 56 | -------------------------------------------------------------------------------- /internal/cmd/build.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "os" 7 | "os/exec" 8 | fp "path/filepath" 9 | "strings" 10 | 11 | "github.com/go-qamel/qamel/internal/config" 12 | "github.com/go-qamel/qamel/internal/generator" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func buildCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "build", 19 | Short: "Build QML app", 20 | Args: cobra.NoArgs, 21 | Run: buildHandler, 22 | } 23 | 24 | cmd.Flags().StringP("output", "o", "", "location for executable file") 25 | cmd.Flags().StringP("profile", "p", "", "profile that used for building app") 26 | cmd.Flags().StringSliceP("tags", "t", []string{}, "space-separated list of build tags to satisfied during the build") 27 | cmd.Flags().Bool("copy-deps", false, "copy dependencies for app with dynamic linking") 28 | cmd.Flags().Bool("skip-vendoring", false, "if uses Go module, skip updating project's vendor") 29 | 30 | return cmd 31 | } 32 | 33 | func buildHandler(cmd *cobra.Command, args []string) { 34 | cBlueBold.Println("Starting build process.") 35 | fmt.Println() 36 | 37 | // Read flags 38 | buildTags, _ := cmd.Flags().GetStringSlice("tags") 39 | outputPath, _ := cmd.Flags().GetString("output") 40 | profileName, _ := cmd.Flags().GetString("profile") 41 | copyDependencies, _ := cmd.Flags().GetBool("copy-deps") 42 | skipVendoring, _ := cmd.Flags().GetBool("skip-vendoring") 43 | 44 | // Load config file 45 | fmt.Print("Load config file...") 46 | 47 | profileName = strings.TrimSpace(profileName) 48 | if profileName == "" { 49 | profileName = "default" 50 | } 51 | 52 | profile, err := config.LoadProfile(configPath, profileName) 53 | if err != nil { 54 | fmt.Println() 55 | cRedBold.Println("Failed to load profile file:", err) 56 | cRedBold.Println("You might need to run `qamel profile setup` again.") 57 | os.Exit(1) 58 | } 59 | cGreen.Println("done") 60 | 61 | // Get project directory in workdir 62 | projectDir, err := os.Getwd() 63 | if err != nil { 64 | cRedBold.Println("Failed to get current working dir:", err) 65 | os.Exit(1) 66 | } 67 | 68 | // If this project uses Go module, make sure to use vendor. 69 | // This is because `qamel build` works by generating binding code 70 | // in project dir and Qamel dir as well. This is done every time 71 | // user build his app, because every user might have different 72 | // profile and config on each build. In old times, the Qamel dir 73 | // in $GOPATH/src is easily accessible and modified. However, 74 | // in current Go module, the Qamel dir in $GOPATH/pkg/mod is read 75 | // only, which make it impossible to generate binding code there. 76 | // Therefore, as workaround, Qamel in Go module *MUST* be used in 77 | // vendor by using `go mod vendor`. 78 | vendorDir := fp.Join(projectDir, "vendor", "github.com", "go-qamel", "qamel") 79 | goModFile := fp.Join(projectDir, "go.mod") 80 | usesGoModule := fileExists(goModFile) 81 | 82 | if usesGoModule && (!dirExists(vendorDir) || !skipVendoring) { 83 | fmt.Print("Generating vendor files...") 84 | 85 | cmdModVendor := exec.Command("go", "mod", "vendor") 86 | cmdOutput, err := cmdModVendor.CombinedOutput() 87 | if err != nil { 88 | fmt.Println() 89 | cRedBold.Println("Failed to vendor app:", err) 90 | cRedBold.Println(string(cmdOutput)) 91 | os.Exit(1) 92 | } 93 | 94 | cGreen.Println("done") 95 | } 96 | 97 | // If vendor doesn't exist, uses Qamel dir in GOPATH 98 | qamelDir := vendorDir 99 | if !dirExists(qamelDir) { 100 | gopath := os.Getenv("GOPATH") 101 | if gopath == "" { 102 | gopath = build.Default.GOPATH 103 | } 104 | qamelDir = fp.Join(gopath, "src", "github.com", "go-qamel", "qamel") 105 | } 106 | 107 | // Make sure the Qamel directory exists 108 | if !dirExists(qamelDir) { 109 | cRedBold.Println("Failed to access qamel: source directory doesn't exist") 110 | os.Exit(1) 111 | } 112 | 113 | // Remove old qamel files 114 | fmt.Print("Removing old build files...") 115 | err = removeQamelFiles(projectDir) 116 | if err != nil { 117 | fmt.Println() 118 | cRedBold.Println("Failed to remove old build files:", err) 119 | os.Exit(1) 120 | } 121 | cGreen.Println("done") 122 | 123 | os.Remove(fp.Join(projectDir, "qamel-icon.syso")) 124 | os.Remove(fp.Join(qamelDir, "moc-viewer.h")) 125 | os.Remove(fp.Join(qamelDir, "moc-listmodel.h")) 126 | os.Remove(fp.Join(qamelDir, "moc-tablemodel.h")) 127 | os.Remove(fp.Join(qamelDir, "qamel_plugin_import.cpp")) 128 | os.Remove(fp.Join(qamelDir, "qamel_qml_plugin_import.cpp")) 129 | 130 | // Generate cgo file and moc for binding in qamel directory 131 | fmt.Print("Generating binding files...") 132 | filesToMoc := []string{"viewer.cpp", "listmodel.h", "tablemodel.h"} 133 | 134 | for _, fileToMoc := range filesToMoc { 135 | fileToMoc = fp.Join(qamelDir, fileToMoc) 136 | if !fileExists(fileToMoc) { 137 | continue 138 | } 139 | 140 | err = generator.CreateMocFile(profile.Moc, fileToMoc) 141 | if err != nil { 142 | fmt.Println() 143 | cRedBold.Printf("Failed to create moc file for %s: %v\n", fileToMoc, err) 144 | os.Exit(1) 145 | } 146 | } 147 | 148 | err = generator.CreateCgoFile(profile, qamelDir, "") 149 | if err != nil { 150 | fmt.Println() 151 | cRedBold.Println("Failed to create cgo file:", err) 152 | os.Exit(1) 153 | } 154 | cGreen.Println("done") 155 | 156 | // Create rcc file 157 | fmt.Print("Generating Qt resource file...") 158 | err = generator.CreateRccFile(profile, projectDir) 159 | if err != nil { 160 | if err == generator.ErrNoResourceDir { 161 | cYellow.Println(err) 162 | } else { 163 | fmt.Println() 164 | cRedBold.Println("Failed to create Qt resource file:", err) 165 | os.Exit(1) 166 | } 167 | } else { 168 | cGreen.Println("done") 169 | } 170 | 171 | // Create syso file 172 | if profile.OS == "windows" && profile.Windres != "" { 173 | fmt.Print("Generating syso file...") 174 | err = generator.CreateSysoFile(profile, projectDir) 175 | if err != nil { 176 | if err == generator.ErrNoIcon { 177 | cYellow.Println(err) 178 | } else { 179 | fmt.Println() 180 | cRedBold.Println("Failed to create syso file:", err) 181 | os.Exit(1) 182 | } 183 | } else { 184 | cGreen.Println("done") 185 | } 186 | } 187 | 188 | // Generate code for QmlObject structs 189 | fmt.Print("Generating code for QML objects...") 190 | errs := generator.CreateQmlObjectCode(profile, projectDir, buildTags...) 191 | if errs != nil && len(errs) > 0 { 192 | fmt.Println() 193 | for _, err := range errs { 194 | cRedBold.Println("Failed:", err) 195 | } 196 | os.Exit(1) 197 | } 198 | cGreen.Println("done") 199 | 200 | // Prepare default output path 201 | if outputPath == "" { 202 | outputPath = fp.Join(projectDir, fp.Base(projectDir)) 203 | if profile.OS == "windows" { 204 | outputPath += ".exe" 205 | } 206 | } 207 | 208 | // Run go build 209 | fmt.Print("Building app...") 210 | cmdArgs := []string{"build", "-o", outputPath} 211 | 212 | if usesGoModule { 213 | cmdArgs = append(cmdArgs, "-mod", "vendor") 214 | } 215 | 216 | if len(buildTags) > 0 { 217 | cmdArgs = append(cmdArgs, "-tags") 218 | cmdArgs = append(cmdArgs, buildTags...) 219 | } 220 | 221 | ldFlags := "all=-s -w" 222 | if profile.OS == "windows" { 223 | ldFlags += " -H=windowsgui" 224 | } 225 | 226 | cmdArgs = append(cmdArgs, "-ldflags") 227 | cmdArgs = append(cmdArgs, ldFlags) 228 | 229 | cmdGo := exec.Command("go", cmdArgs...) 230 | cmdGo.Dir = projectDir 231 | cmdGo.Env = append(os.Environ(), 232 | `CGO_ENABLED=1`, 233 | `CGO_CFLAGS_ALLOW=.*`, 234 | `CGO_CXXFLAGS_ALLOW=.*`, 235 | `CGO_LDFLAGS_ALLOW=.*`, 236 | "GOOS="+profile.OS, 237 | "GOARCH="+profile.Arch, 238 | "CC="+profile.Gcc, 239 | "CXX="+profile.Gxx) 240 | 241 | cmdOutput, err := cmdGo.CombinedOutput() 242 | if err != nil { 243 | fmt.Println() 244 | cRedBold.Println("Failed to build app:", err) 245 | cRedBold.Println(string(cmdOutput)) 246 | os.Exit(1) 247 | } 248 | cGreen.Println("done") 249 | 250 | // If it's shared mode, copy dependencies 251 | if !profile.Static && copyDependencies { 252 | fmt.Print("Copying dependencies...") 253 | err = generator.CopyDependencies(profile, projectDir, outputPath) 254 | if err != nil { 255 | fmt.Println() 256 | cRedBold.Println("Failed to copy dependencies:", err) 257 | os.Exit(1) 258 | } 259 | cGreen.Println("done") 260 | } 261 | 262 | // Build finished 263 | fmt.Println() 264 | cBlueBold.Println("Build finished succesfully.") 265 | } 266 | 267 | // removeQamelFiles remove old generated qamel files in the specified dir 268 | func removeQamelFiles(rootDir string) error { 269 | prefixes := []string{"moc-qamel-", "qamel-"} 270 | 271 | err := fp.Walk(rootDir, func(path string, info os.FileInfo, err error) error { 272 | if info.IsDir() { 273 | if strings.HasPrefix(info.Name(), ".") { 274 | return fp.SkipDir 275 | } 276 | return nil 277 | } 278 | 279 | switch fileExt := fp.Ext(info.Name()); fileExt { 280 | case ".h", ".go", ".cpp": 281 | default: 282 | return nil 283 | } 284 | 285 | if strings.HasSuffix(info.Name(), "_plugin_import.cpp") { 286 | return os.Remove(path) 287 | } 288 | 289 | for i := range prefixes { 290 | if strings.HasPrefix(info.Name(), prefixes[i]) { 291 | return os.Remove(path) 292 | } 293 | } 294 | 295 | return nil 296 | }) 297 | 298 | return err 299 | } 300 | -------------------------------------------------------------------------------- /internal/cmd/docker.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "os" 7 | "os/exec" 8 | "os/user" 9 | fp "path/filepath" 10 | "runtime" 11 | "strings" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func dockerCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "docker [target]", 19 | Run: dockerHandler, 20 | Args: cobra.ExactArgs(1), 21 | Short: "Build QML app using Docker image", 22 | Long: "Build QML app using Docker image.\nPossible values are " + 23 | `"linux", "linux-static", "win32", "win32-static", "win64" and "win64-static".`, 24 | } 25 | 26 | cmd.Flags().StringP("output", "o", "", "location for executable file") 27 | cmd.Flags().StringSliceP("tags", "t", []string{}, "space-separated list of build tags to satisfied during the build") 28 | cmd.Flags().Bool("copy-deps", false, "copy dependencies for app with dynamic linking") 29 | cmd.Flags().Bool("skip-vendoring", false, "if uses Go module, skip updating project's vendor") 30 | 31 | return cmd 32 | } 33 | 34 | func dockerHandler(cmd *cobra.Command, args []string) { 35 | cBlueBold.Println("Run `qamel build` from Docker image.") 36 | 37 | // Read flags 38 | buildTags, _ := cmd.Flags().GetStringSlice("tags") 39 | outputPath, _ := cmd.Flags().GetString("output") 40 | copyDependencies, _ := cmd.Flags().GetBool("copy-deps") 41 | skipVendoring, _ := cmd.Flags().GetBool("skip-vendoring") 42 | 43 | // Get target name 44 | target := args[0] 45 | switch target { 46 | case "linux", "linux-static", 47 | "win32", "win32-static", 48 | "win64", "win64-static": 49 | default: 50 | cRedBold.Printf("Target %s is not supported.\n", target) 51 | os.Exit(1) 52 | } 53 | 54 | // Get gopath 55 | gopath := os.Getenv("GOPATH") 56 | if gopath == "" { 57 | gopath = build.Default.GOPATH 58 | } 59 | 60 | // Get project directory from current working dir 61 | projectDir, err := os.Getwd() 62 | if err != nil { 63 | cRedBold.Println("Failed to get current working dir:", err) 64 | os.Exit(1) 65 | } 66 | 67 | // If this project uses Go module, vendor it first before 68 | // passing it to Docker 69 | vendorDir := fp.Join(projectDir, "vendor", "github.com", "go-qamel", "qamel") 70 | goModFile := fp.Join(projectDir, "go.mod") 71 | usesGoModule := fileExists(goModFile) 72 | 73 | if usesGoModule && (!dirExists(vendorDir) || !skipVendoring) { 74 | fmt.Print("Generating vendor files...") 75 | 76 | cmdModVendor := exec.Command("go", "mod", "vendor") 77 | cmdOutput, err := cmdModVendor.CombinedOutput() 78 | if err != nil { 79 | fmt.Println() 80 | cRedBold.Println("Failed to vendor app:", err) 81 | cRedBold.Println(string(cmdOutput)) 82 | os.Exit(1) 83 | } 84 | 85 | cGreen.Println("done") 86 | } 87 | 88 | // Get docker user from current active user 89 | currentUser, err := user.Current() 90 | if err != nil { 91 | cRedBold.Println("Failed to get user's data:", err) 92 | os.Exit(1) 93 | } 94 | 95 | uid := currentUser.Uid 96 | gid := currentUser.Gid 97 | dockerUser := fmt.Sprintf("%s:%s", uid, gid) 98 | 99 | if runtime.GOOS == "windows" { 100 | uidParts := strings.Split(uid, "-") 101 | dockerUser = uidParts[len(uidParts)-1] 102 | } 103 | 104 | // Create directory for Go build's cache 105 | goCacheDir := fp.Join(projectDir, ".qamel-cache", target, "go") 106 | 107 | err = os.MkdirAll(goCacheDir, os.ModePerm) 108 | if err != nil { 109 | cRedBold.Println("Failed to create cache directory for Go build:", err) 110 | os.Exit(1) 111 | } 112 | 113 | // If target is Linux, create ccache dir as well 114 | ccacheDir := "" 115 | if strings.HasPrefix(target, "linux") { 116 | ccacheDir = fp.Join(projectDir, ".qamel-cache", target, "ccache") 117 | 118 | err = os.MkdirAll(ccacheDir, os.ModePerm) 119 | if err != nil { 120 | cRedBold.Println("Failed to create cache directory for gcc build:", err) 121 | os.Exit(1) 122 | } 123 | } 124 | 125 | // Prepare docker arguments 126 | dockerGopath := unixJoinPath("/", "home", "user", "go") 127 | dockerProjectDir := unixJoinPath(dockerGopath, "src", fp.Base(projectDir)) 128 | dockerGoCacheDir := unixJoinPath("/", "home", "user", ".cache", "go-build") 129 | 130 | dockerBindProject := fmt.Sprintf(`type=bind,src=%s,dst=%s`, 131 | projectDir, dockerProjectDir) 132 | dockerBindGoSrc := fmt.Sprintf(`type=bind,src=%s,dst=%s`, 133 | unixJoinPath(gopath, "src"), 134 | unixJoinPath(dockerGopath, "src")) 135 | dockerBindGoCache := fmt.Sprintf(`type=bind,src=%s,dst=%s`, 136 | unixJoinPath(goCacheDir), dockerGoCacheDir) 137 | 138 | dockerArgs := []string{"run", "--rm", 139 | "--attach", "stdout", 140 | "--attach", "stderr", 141 | "--user", dockerUser, 142 | "--workdir", dockerProjectDir, 143 | "--mount", dockerBindProject, 144 | "--mount", dockerBindGoSrc, 145 | "--mount", dockerBindGoCache} 146 | 147 | if ccacheDir != "" { 148 | dockerCcacheDir := unixJoinPath("/", "home", "user", ".ccache") 149 | dockerBindCcache := fmt.Sprintf(`type=bind,src=%s,dst=%s`, 150 | unixJoinPath(ccacheDir), dockerCcacheDir) 151 | 152 | dockerArgs = append(dockerArgs, "--mount", dockerBindCcache) 153 | } 154 | 155 | // If uses go module, set env GO111MODULE to on 156 | if usesGoModule { 157 | dockerArgs = append(dockerArgs, "--env", "GO111MODULE=on") 158 | } 159 | 160 | // Finally, set image name 161 | dockerImageName := fmt.Sprintf("radhifadlillah/qamel:%s", target) 162 | dockerArgs = append(dockerArgs, dockerImageName) 163 | 164 | // Add qamel arguments 165 | dockerArgs = append(dockerArgs, "--skip-vendoring") 166 | 167 | if outputPath != "" { 168 | dockerArgs = append(dockerArgs, "-o", outputPath) 169 | } 170 | 171 | if len(buildTags) > 0 { 172 | dockerArgs = append(dockerArgs, "-t") 173 | dockerArgs = append(dockerArgs, buildTags...) 174 | } 175 | 176 | if copyDependencies { 177 | dockerArgs = append(dockerArgs, "--copy-deps") 178 | } 179 | 180 | // Run docker 181 | cmdDocker := exec.Command("docker", dockerArgs...) 182 | cmdDocker.Stdout = os.Stdout 183 | cmdDocker.Stderr = os.Stderr 184 | 185 | err = cmdDocker.Start() 186 | if err != nil { 187 | cRedBold.Println("Failed to start Docker:", err) 188 | os.Exit(1) 189 | } 190 | 191 | err = cmdDocker.Wait() 192 | if err != nil { 193 | cRedBold.Println("Failed to build app using Docker:", err) 194 | os.Exit(1) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /internal/cmd/profile-delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/go-qamel/qamel/internal/config" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func profileDeleteCmd() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "delete", 14 | Short: "Delete an existing profile", 15 | Args: cobra.MinimumNArgs(1), 16 | Aliases: []string{"rm"}, 17 | Run: profileDeleteHandler, 18 | } 19 | } 20 | 21 | func profileDeleteHandler(cmd *cobra.Command, args []string) { 22 | // Load existing profiles 23 | profiles, err := config.LoadProfiles(configPath) 24 | if err != nil { 25 | cRedBold.Println("Failed to remove the profile:", err) 26 | os.Exit(1) 27 | } 28 | 29 | // Delete each profile 30 | for _, profileName := range args { 31 | delete(profiles, profileName) 32 | } 33 | 34 | // Save the new profiles after removal 35 | err = config.SaveProfiles(configPath, profiles) 36 | if err != nil { 37 | cRedBold.Println("Failed to remove the profile:", err) 38 | os.Exit(1) 39 | } 40 | 41 | logFormat := "Profile %s has been removed\n" 42 | if len(args) > 1 { 43 | logFormat = "Profiles [%s] have been removed\n" 44 | } 45 | 46 | cBlueBold.Printf(logFormat, strings.Join(args, ",")) 47 | } 48 | -------------------------------------------------------------------------------- /internal/cmd/profile-list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | 8 | "github.com/go-qamel/qamel/internal/config" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func profileListCmd() *cobra.Command { 13 | return &cobra.Command{ 14 | Use: "list", 15 | Short: "List all existing profile", 16 | Args: cobra.NoArgs, 17 | Aliases: []string{"ls"}, 18 | Run: profileListHandler, 19 | } 20 | } 21 | 22 | func profileListHandler(cmd *cobra.Command, args []string) { 23 | // Load existing profiles 24 | profiles, err := config.LoadProfiles(configPath) 25 | if err != nil { 26 | cRedBold.Println("Failed to get list of profile:", err) 27 | os.Exit(1) 28 | } 29 | 30 | // Get list of profile name 31 | var names []string 32 | for key := range profiles { 33 | names = append(names, key) 34 | } 35 | 36 | sort.Strings(names) 37 | for i := 0; i < len(names); i++ { 38 | fmt.Println(names[i]) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/cmd/profile-print.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-qamel/qamel/internal/config" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func profilePrintCmd() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "print [profileName]", 14 | Short: "Print data of specified profile", 15 | Args: cobra.MaximumNArgs(1), 16 | Run: profilePrintHandler, 17 | } 18 | } 19 | 20 | func profilePrintHandler(cmd *cobra.Command, args []string) { 21 | // Get profile name 22 | profileName := "default" 23 | if len(args) == 1 { 24 | profileName = args[0] 25 | } 26 | 27 | // Load existing profiles 28 | profiles, err := config.LoadProfiles(configPath) 29 | if err != nil { 30 | cRedBold.Println("Failed to print the profile:", err) 31 | os.Exit(1) 32 | } 33 | 34 | // Save the profiles with removed item 35 | profile, ok := profiles[profileName] 36 | if !ok { 37 | cRedBold.Printf("Failed to print the profile: %s not exists\n", profileName) 38 | os.Exit(1) 39 | } 40 | 41 | cBlueBold.Printf("Details of profile %s\n", profileName) 42 | cCyanBold.Print("OS : ") 43 | fmt.Println(profile.OS) 44 | cCyanBold.Print("Arch : ") 45 | fmt.Println(profile.Arch) 46 | cCyanBold.Print("Static : ") 47 | fmt.Println(profile.Static) 48 | cCyanBold.Print("Qmake : ") 49 | fmt.Println(profile.Qmake) 50 | cCyanBold.Print("Moc : ") 51 | fmt.Println(profile.Moc) 52 | cCyanBold.Print("Rcc : ") 53 | fmt.Println(profile.Rcc) 54 | cCyanBold.Print("Gcc : ") 55 | fmt.Println(profile.Gcc) 56 | cCyanBold.Print("G++ : ") 57 | fmt.Println(profile.Gxx) 58 | 59 | if profile.OS == "windows" { 60 | if !profile.Static { 61 | cCyanBold.Print("Objdump : ") 62 | fmt.Println(profile.Objdump) 63 | } 64 | 65 | cCyanBold.Print("Windres : ") 66 | fmt.Println(profile.Windres) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/cmd/profile-setup.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | fp "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/go-qamel/qamel/internal/config" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func profileSetupCmd() *cobra.Command { 16 | return &cobra.Command{ 17 | Use: "setup [profileName]", 18 | Short: "Set up a new or existing profile", 19 | Args: cobra.MaximumNArgs(1), 20 | Run: profileSetupHandler, 21 | } 22 | } 23 | 24 | func profileSetupHandler(cmd *cobra.Command, args []string) { 25 | // Get profile name 26 | profileName := "default" 27 | if len(args) > 0 { 28 | profileName = args[0] 29 | } 30 | 31 | // Define input reader 32 | reader := bufio.NewReader(os.Stdin) 33 | 34 | // Fetch target OS 35 | cBlueBold.Println("Thanks for using qamel, QML's binding for Go.") 36 | fmt.Println() 37 | fmt.Println("Please specify the target OS for this profile. " + 38 | `Possible values are "windows", "linux" and "darwin". ` + "\n" + 39 | "Keep it empty to use your system OS.") 40 | fmt.Println() 41 | 42 | cCyanBold.Printf("Target OS (default %s) : ", runtime.GOOS) 43 | targetOS, _ := reader.ReadString('\n') 44 | targetOS = strings.TrimSpace(targetOS) 45 | if targetOS == "" { 46 | targetOS = runtime.GOOS 47 | } 48 | 49 | switch targetOS { 50 | case "linux", "windows", "darwin": 51 | default: 52 | cRedBold.Printf("OS %s is not supported\n", targetOS) 53 | os.Exit(1) 54 | } 55 | 56 | // Fetch target architecture 57 | fmt.Println() 58 | fmt.Println("Please specify the target architecture for this profile. " + 59 | `Possible values are "386" and "amd64".` + "\n" + 60 | "Keep it empty to use your system architecture.") 61 | fmt.Println() 62 | 63 | cCyanBold.Printf("Target arch (default %s) : ", runtime.GOARCH) 64 | targetArch, _ := reader.ReadString('\n') 65 | targetArch = strings.TrimSpace(targetArch) 66 | if targetArch == "" { 67 | targetArch = runtime.GOARCH 68 | } 69 | 70 | switch targetArch { 71 | case "386", "amd64": 72 | default: 73 | cRedBold.Printf("Architecture %s is not supported\n", targetArch) 74 | os.Exit(1) 75 | } 76 | 77 | // Fetch build mode 78 | fmt.Println() 79 | fmt.Println("Please specify whether this profile used to build static or shared app.") 80 | fmt.Println() 81 | 82 | cCyanBold.Print("Use static mode (y/n, default n) : ") 83 | staticMode, _ := reader.ReadString('\n') 84 | staticMode = strings.TrimSpace(staticMode) 85 | staticMode = strings.ToLower(staticMode) 86 | if staticMode == "" { 87 | staticMode = "n" 88 | } 89 | 90 | if staticMode != "y" && staticMode != "n" { 91 | cRedBold.Println("Input value is not valid") 92 | os.Exit(1) 93 | } 94 | 95 | // Fetch path to Qt's directory and tools 96 | fmt.Println() 97 | fmt.Println("Please specify the *full path* to your Qt's tools directory.") 98 | fmt.Println("This might be different depending on your platform or your target. " + 99 | "For example, in Linux with Qt 5.11.1, " + 100 | "the tools are located in $HOME/Qt5.11.1/5.11.1/gcc_64/bin/") 101 | fmt.Println() 102 | 103 | cCyanBold.Print("Qt tools dir : ") 104 | qtDir, _ := reader.ReadString('\n') 105 | qtDir = strings.TrimSpace(qtDir) 106 | if !dirExists(qtDir) { 107 | cRedBold.Println("The specified directory does not exist") 108 | os.Exit(1) 109 | } 110 | 111 | // Make sure qmake, moc, and rcc is exists 112 | qmakePath := fp.Join(qtDir, "qmake") 113 | mocPath := fp.Join(qtDir, "moc") 114 | rccPath := fp.Join(qtDir, "rcc") 115 | if runtime.GOOS == "windows" { 116 | qmakePath += ".exe" 117 | mocPath += ".exe" 118 | rccPath += ".exe" 119 | } 120 | 121 | qmakeExists := fileExists(qmakePath) 122 | mocExists := fileExists(mocPath) 123 | rccExists := fileExists(rccPath) 124 | 125 | cCyanBold.Print("qmake : ") 126 | if qmakeExists { 127 | cGreen.Println("found") 128 | } else { 129 | cRed.Println("not found") 130 | } 131 | 132 | cCyanBold.Print("moc : ") 133 | if mocExists { 134 | cGreen.Println("found") 135 | } else { 136 | cRed.Println("not found") 137 | } 138 | 139 | cCyanBold.Print("rcc : ") 140 | if rccExists { 141 | cGreen.Println("found") 142 | } else { 143 | cRed.Println("not found") 144 | } 145 | 146 | if !qmakeExists || !mocExists || !rccExists { 147 | fmt.Println() 148 | fmt.Println("Unable to find some of the tools. Please specify the *full path* to it manually.") 149 | fmt.Println() 150 | 151 | if !qmakeExists { 152 | cCyanBold.Print("Path to qmake : ") 153 | qmakePath, _ = reader.ReadString('\n') 154 | qmakePath = strings.TrimSpace(qmakePath) 155 | if !fileExists(qmakePath) { 156 | cRedBold.Println("The specified path does not exist") 157 | os.Exit(1) 158 | } 159 | } 160 | 161 | if !mocExists { 162 | cCyanBold.Print("Path to moc : ") 163 | mocPath, _ = reader.ReadString('\n') 164 | mocPath = strings.TrimSpace(mocPath) 165 | if !fileExists(mocPath) { 166 | cRedBold.Println("The specified path does not exist") 167 | os.Exit(1) 168 | } 169 | } 170 | 171 | if !rccExists { 172 | cCyanBold.Print("Path to rcc : ") 173 | rccPath, _ = reader.ReadString('\n') 174 | rccPath = strings.TrimSpace(rccPath) 175 | if !fileExists(rccPath) { 176 | cRedBold.Println("The specified path does not exist") 177 | os.Exit(1) 178 | } 179 | } 180 | } 181 | 182 | // Fetch custom C and C++ compiler 183 | defaultGcc := "gcc" 184 | defaultGxx := "g++" 185 | defaultObjdump := "objdump" 186 | if runtime.GOOS == "windows" { 187 | defaultGcc += ".exe" 188 | defaultGxx += ".exe" 189 | defaultObjdump += ".exe" 190 | } 191 | 192 | fmt.Println() 193 | fmt.Println("Please specify the *full path* to your compiler.") 194 | fmt.Println("Keep it empty to use the default compiler on your system.") 195 | fmt.Println() 196 | 197 | cCyanBold.Printf("C compiler (default %s) : ", defaultGcc) 198 | gccPath, _ := reader.ReadString('\n') 199 | gccPath = strings.TrimSpace(gccPath) 200 | if gccPath == "" { 201 | gccPath = defaultGcc 202 | } 203 | 204 | cCyanBold.Printf("C++ compiler (default %s) : ", defaultGxx) 205 | gxxPath, _ := reader.ReadString('\n') 206 | gxxPath = strings.TrimSpace(gxxPath) 207 | if gxxPath == "" { 208 | gxxPath = defaultGxx 209 | } 210 | 211 | // If Windows shared, specify path to objdump, which used to fetch dependencies 212 | objdumpPath := "" 213 | if targetOS == "windows" && staticMode != "y" { 214 | cCyanBold.Printf("Objdump (default %s) : ", defaultObjdump) 215 | objdumpPath, _ = reader.ReadString('\n') 216 | objdumpPath = strings.TrimSpace(objdumpPath) 217 | if objdumpPath == "" { 218 | objdumpPath = defaultObjdump 219 | } 220 | } 221 | 222 | // If Windows, specify path to windres 223 | // which will be used to create icon for the executable 224 | windresPath := "" 225 | if targetOS == "windows" { 226 | fmt.Println() 227 | fmt.Println("Since you are targeting Windows, you might want to set icon for your executable file. " + 228 | "To do so, please specify the *full path* to windres on your system. " + 229 | "It's usually located in the directory where MinGW is installed.") 230 | fmt.Println() 231 | 232 | cCyanBold.Print("Path to windres : ") 233 | windresPath, _ = reader.ReadString('\n') 234 | windresPath = strings.TrimSpace(windresPath) 235 | if !fileExists(windresPath) { 236 | cRedBold.Println("The specified path does not exist") 237 | os.Exit(1) 238 | } 239 | } 240 | 241 | // Save config file 242 | fmt.Printf("Saving profile %s...", profileName) 243 | 244 | profiles, err := config.LoadProfiles(configPath) 245 | if err != nil { 246 | fmt.Println() 247 | cRedBold.Println("Failed to save the profile:", err) 248 | os.Exit(1) 249 | } 250 | 251 | profiles[profileName] = config.Profile{ 252 | OS: targetOS, 253 | Arch: targetArch, 254 | Static: staticMode == "y", 255 | Qmake: qmakePath, 256 | Moc: mocPath, 257 | Rcc: rccPath, 258 | Gcc: gccPath, 259 | Gxx: gxxPath, 260 | Windres: windresPath, 261 | Objdump: objdumpPath, 262 | } 263 | 264 | err = config.SaveProfiles(configPath, profiles) 265 | if err != nil { 266 | fmt.Println() 267 | cRedBold.Println("Failed to save the profile:", err) 268 | os.Exit(1) 269 | } 270 | 271 | cGreen.Println("done") 272 | 273 | // Setup finished 274 | fmt.Println() 275 | cBlueBold.Println("Setup finished.") 276 | cBlueBold.Println("Now you can get started on your own QML app.") 277 | } 278 | -------------------------------------------------------------------------------- /internal/cmd/profile.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func profileCmd() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "profile", 10 | Short: "Manage profiles for QML's binding", 11 | Args: cobra.NoArgs, 12 | } 13 | 14 | cmd.AddCommand( 15 | profileSetupCmd(), 16 | profileDeleteCmd(), 17 | profileListCmd(), 18 | profilePrintCmd(), 19 | ) 20 | 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | ap "github.com/muesli/go-app-paths" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | configPath = "config.json" 12 | ) 13 | 14 | func init() { 15 | // Get config path in ${XDG_CONFIG_HOME}/qamel/config.json 16 | var err error 17 | userScope := ap.NewScope(ap.User, "qamel") 18 | configPath, err = userScope.ConfigPath("config.json") 19 | if err != nil { 20 | log.Fatalln(err) 21 | } 22 | } 23 | 24 | // QamelCmd returns the root command for qamel 25 | func QamelCmd() *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "qamel", 28 | Short: "qamel is tools and binding for creating GUI app in Go using Qt + QML", 29 | Args: cobra.NoArgs, 30 | } 31 | 32 | cmd.AddCommand( 33 | buildCmd(), 34 | dockerCmd(), 35 | profileCmd(), 36 | ) 37 | 38 | return cmd 39 | } 40 | -------------------------------------------------------------------------------- /internal/cmd/utils.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | fp "path/filepath" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var ( 12 | cCyan = color.New(color.FgCyan) 13 | cCyanBold = color.New(color.FgCyan, color.Bold) 14 | cRed = color.New(color.FgRed) 15 | cRedBold = color.New(color.FgRed, color.Bold) 16 | cGreen = color.New(color.FgGreen) 17 | cGreenBold = color.New(color.FgGreen, color.Bold) 18 | cBlueBold = color.New(color.FgBlue, color.Bold) 19 | cYellow = color.New(color.FgYellow) 20 | ) 21 | 22 | // fileExists checks if the file in specified path is exists 23 | func fileExists(filePath string) bool { 24 | info, err := os.Stat(filePath) 25 | return !os.IsNotExist(err) && !info.IsDir() 26 | } 27 | 28 | // dirExists checks if the directory in specified path is exists 29 | func dirExists(dirPath string) bool { 30 | info, err := os.Stat(dirPath) 31 | return !os.IsNotExist(err) && info.IsDir() 32 | } 33 | 34 | // unixJoinPath is like filepath.Join, but the separator will always using front slash (/) 35 | func unixJoinPath(elem ...string) string { 36 | result := fp.ToSlash(fp.Join(elem...)) 37 | 38 | // Convert C:/dir/name to /c/dir/name 39 | volName := fp.VolumeName(result) 40 | if volName != "" && strings.HasPrefix(result, volName) { 41 | newVolName := "/" + strings.ToLower(volName) 42 | newVolName = strings.TrimSuffix(newVolName, ":") 43 | result = newVolName + strings.TrimPrefix(result, volName) 44 | } 45 | 46 | return result 47 | } 48 | -------------------------------------------------------------------------------- /internal/config/profile.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | fp "path/filepath" 8 | ) 9 | 10 | // Profile is struct containing path to qmake, moc, rcc, C compiler and C++ compiler 11 | type Profile struct { 12 | OS string 13 | Arch string 14 | Static bool 15 | 16 | Qmake string 17 | Moc string 18 | Rcc string 19 | Gcc string 20 | Gxx string 21 | Windres string 22 | Objdump string 23 | } 24 | 25 | // LoadProfiles load all profiles inside config file in the specified configPath. 26 | func LoadProfiles(configPath string) (map[string]Profile, error) { 27 | // If config file doesn't exist, return empty map 28 | if !fileExists(configPath) { 29 | return map[string]Profile{}, nil 30 | } 31 | 32 | // Open file 33 | configFile, err := os.Open(configPath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | defer configFile.Close() 38 | 39 | // Decode JSON 40 | profiles := map[string]Profile{} 41 | err = json.NewDecoder(configFile).Decode(&profiles) 42 | return profiles, err 43 | } 44 | 45 | // LoadProfile loads profile with specified name from config file. 46 | func LoadProfile(configPath string, name string) (Profile, error) { 47 | profiles, err := LoadProfiles(configPath) 48 | if err != nil { 49 | return Profile{}, err 50 | } 51 | 52 | if prof, ok := profiles[name]; ok { 53 | return prof, nil 54 | } 55 | 56 | return Profile{}, fmt.Errorf("profile %s doesn't exist", name) 57 | } 58 | 59 | // SaveProfiles saves the profile as JSON in the specified configPath. 60 | func SaveProfiles(configPath string, profiles map[string]Profile) error { 61 | // Make sure config dir is exists 62 | os.MkdirAll(fp.Dir(configPath), os.ModePerm) 63 | 64 | // Create file 65 | configFile, err := os.Create(configPath) 66 | if err != nil { 67 | return err 68 | } 69 | defer configFile.Close() 70 | 71 | // Encode profiles to JSON 72 | return json.NewEncoder(configFile).Encode(&profiles) 73 | } 74 | 75 | func fileExists(filePath string) bool { 76 | info, err := os.Stat(filePath) 77 | return !os.IsNotExist(err) && !info.IsDir() 78 | } 79 | -------------------------------------------------------------------------------- /internal/generator/cgo.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | fp "path/filepath" 9 | "regexp" 10 | "runtime" 11 | "strings" 12 | 13 | "github.com/go-qamel/qamel/internal/config" 14 | ) 15 | 16 | var ( 17 | rxMakefile = regexp.MustCompile(`^(\S+)\s*=\s*(.+)$`) 18 | rxCompilerVar = regexp.MustCompile(`\$\((\S+)\)`) 19 | ) 20 | 21 | // CreateCgoFile creates cgo file in specified package, 22 | // using cgo flags that generated by CreateCgoFlags(). 23 | func CreateCgoFile(profile config.Profile, projectDir string, pkgName string) error { 24 | // Make sure project directory is exists 25 | err := os.MkdirAll(projectDir, os.ModePerm) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // Get the package name 31 | if pkgName == "" { 32 | pkgName, err = getPackageNameFromDir(projectDir) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | 38 | // Create cgo flags 39 | cgoFlags, err := createCgoFlags(profile, projectDir) 40 | if err != nil { 41 | return fmt.Errorf("failed to create cgo flags: %v", err) 42 | } 43 | 44 | // Create destination file 45 | fileName := fp.Join(projectDir, "qamel-cgo-"+pkgName+".go") 46 | fileContent := fmt.Sprintln("package " + pkgName) 47 | fileContent += fmt.Sprintln() 48 | fileContent += fmt.Sprintln("/*") 49 | fileContent += fmt.Sprintln(cgoFlags) 50 | fileContent += fmt.Sprintln("*/") 51 | fileContent += fmt.Sprintln(`import "C"`) 52 | 53 | return saveToFile(fileName, fileContent) 54 | } 55 | 56 | // createCgoFlags creates cgo flags by using qmake 57 | func createCgoFlags(profile config.Profile, projectDir string) (string, error) { 58 | // Create project file 59 | proContent := "QT += qml quick quickcontrols2 widgets svg\n" 60 | proContent += "CONFIG += release\n" 61 | if profile.OS == "windows" { 62 | proContent += "CONFIG += windows\n" 63 | } 64 | 65 | proFilePath := fp.Join(projectDir, "qamel.pro") 66 | err := saveToFile(proFilePath, proContent) 67 | if err != nil { 68 | return "", err 69 | } 70 | 71 | // Create makefile from project file using qmake 72 | makeFilePath := fp.Join(projectDir, "qamel.makefile") 73 | 74 | qmakeSpec := "" 75 | switch profile.OS { 76 | case "darwin": 77 | qmakeSpec = "macx-clang" 78 | case "linux": 79 | qmakeSpec = "linux-g++" 80 | case "windows": 81 | qmakeSpec = "win32-g++" 82 | } 83 | 84 | gccDir := fp.Dir(profile.Gcc) 85 | gxxDir := fp.Dir(profile.Gxx) 86 | envPath := os.Getenv("PATH") 87 | pathSeparator := ":" 88 | 89 | if runtime.GOOS == "windows" { 90 | pathSeparator = ";" 91 | } 92 | 93 | if fileExists(profile.Gcc) { 94 | envPath = fmt.Sprintf(`%s%s%s`, gccDir, pathSeparator, envPath) 95 | } 96 | 97 | if fileExists(profile.Gxx) && gxxDir != gccDir { 98 | envPath = fmt.Sprintf(`%s%s%s`, gxxDir, pathSeparator, envPath) 99 | } 100 | 101 | cmdQmake := exec.Command(profile.Qmake, "-o", makeFilePath, "-spec", qmakeSpec, proFilePath) 102 | cmdQmake.Dir = projectDir 103 | cmdQmake.Env = append(os.Environ(), "PATH="+envPath) 104 | if btOutput, err := cmdQmake.CombinedOutput(); err != nil { 105 | return "", fmt.Errorf("%v\n%s", err, btOutput) 106 | } 107 | 108 | // Parse makefile 109 | qmakeResultPath := makeFilePath 110 | if profile.OS == "windows" { 111 | qmakeResultPath += ".Release" 112 | } 113 | 114 | mapCompiler := map[string]string{} 115 | makeFile, err := os.Open(qmakeResultPath) 116 | if err != nil { 117 | return "", err 118 | } 119 | defer makeFile.Close() 120 | 121 | scanner := bufio.NewScanner(makeFile) 122 | for scanner.Scan() { 123 | text := scanner.Text() 124 | parts := rxMakefile.FindStringSubmatch(text) 125 | if len(parts) != 3 { 126 | continue 127 | } 128 | 129 | mapCompiler[parts[1]] = parts[2] 130 | } 131 | 132 | if err := scanner.Err(); err != nil { 133 | return "", err 134 | } 135 | 136 | if _, ok := mapCompiler["EXPORT_ARCH_ARGS"]; ok { 137 | if profile.Arch == "amd64" { 138 | mapCompiler["EXPORT_ARCH_ARGS"] = "-arch x86_64" 139 | } else { 140 | mapCompiler["EXPORT_ARCH_ARGS"] = "-arch i386" 141 | } 142 | } 143 | delete(mapCompiler, "EXPORT_ACTIVE_ARCHS") 144 | 145 | // Convert variable in compiler flags 146 | for flagKey, flagValue := range mapCompiler { 147 | variables := rxCompilerVar.FindAllString(flagValue, -1) 148 | for _, variable := range variables { 149 | variableKey := rxCompilerVar.ReplaceAllString(variable, "$1") 150 | variableValue := mapCompiler[variableKey] 151 | flagValue = strings.Replace(flagValue, variable, variableValue, -1) 152 | } 153 | 154 | // Go does not support big-obj files yet (see https://github.com/golang/go/issues/24341). 155 | // However, qmake in mingw64 uses them by default. To bypass it, we need to remove `-Wa,-mbig-obj` flags. 156 | flagValue = strings.Replace(flagValue, " -Wa,-mbig-obj ", " ", -1) 157 | mapCompiler[flagKey] = strings.TrimSpace(flagValue) 158 | } 159 | 160 | // Fetch the needed flags for cgo 161 | cgoFlags := fmt.Sprintf("#cgo CFLAGS: %s\n", mapCompiler["CFLAGS"]) 162 | cgoFlags += fmt.Sprintf("#cgo CXXFLAGS: %s\n", mapCompiler["CXXFLAGS"]) 163 | cgoFlags += fmt.Sprintf("#cgo CXXFLAGS: %s\n", mapCompiler["INCPATH"]) 164 | cgoFlags += fmt.Sprintf("#cgo LDFLAGS: %s\n", mapCompiler["LFLAGS"]) 165 | cgoFlags += fmt.Sprintf("#cgo LDFLAGS: %s\n", mapCompiler["LIBS"]) 166 | cgoFlags += fmt.Sprintln("#cgo CFLAGS: -Wno-unused-parameter -Wno-unused-variable -Wno-return-type") 167 | cgoFlags += fmt.Sprint("#cgo CXXFLAGS: -Wno-unused-parameter -Wno-unused-variable -Wno-return-type") 168 | 169 | // Remove generated file and folder 170 | os.Remove(proFilePath) 171 | os.Remove(makeFilePath) 172 | os.Remove(makeFilePath + ".Debug") 173 | os.Remove(makeFilePath + ".Release") 174 | os.Remove(fp.Join(projectDir, ".qmake.stash")) 175 | 176 | debugDir := fp.Join(projectDir, "debug") 177 | if dirExists(debugDir) && dirEmpty(debugDir) { 178 | os.RemoveAll(debugDir) 179 | } 180 | 181 | releaseDir := fp.Join(projectDir, "release") 182 | if dirExists(releaseDir) && dirEmpty(releaseDir) { 183 | os.RemoveAll(releaseDir) 184 | } 185 | 186 | return cgoFlags, nil 187 | } 188 | -------------------------------------------------------------------------------- /internal/generator/deps-linux.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | fp "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // copyLinuxPlugins copies Linux plugins to output directory 14 | func copyLinuxPlugins(qtPluginsDir string, outputDir string) error { 15 | dstPluginsDir := fp.Join(outputDir, "plugins") 16 | plugins := []string{ 17 | "platforms/libqxcb.so", 18 | "platforminputcontexts", 19 | "imageformats", 20 | "xcbglintegrations", 21 | "iconengines/libqsvgicon.so"} 22 | 23 | for _, plugin := range plugins { 24 | srcPath := fp.Join(qtPluginsDir, plugin) 25 | dstPath := fp.Join(dstPluginsDir, plugin) 26 | 27 | if !fileDirExists(srcPath) { 28 | continue 29 | } 30 | 31 | err := copyFileDir(srcPath, dstPath, nil) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // copyLinuxLibs copies Linux libraries to output directory 41 | func copyLinuxLibs(qtLibsDir string, outputPath string) error { 42 | // Get list of files to check. 43 | // The first one is the output binary file 44 | filesToCheck := []string{outputPath} 45 | 46 | // The plugins and qml libraries (*.so) which copied before 47 | // must be checked as well 48 | outputDir := fp.Dir(outputPath) 49 | fp.Walk(outputDir, func(path string, info os.FileInfo, err error) error { 50 | if info.IsDir() { 51 | return nil 52 | } 53 | 54 | fileExt := fp.Ext(info.Name()) 55 | if strings.HasPrefix(fileExt, ".so") { 56 | filesToCheck = append(filesToCheck, path) 57 | } 58 | 59 | return nil 60 | }) 61 | 62 | // Get list of dependency of the files 63 | mapDependencies := map[string]string{} 64 | filesAlreadyChecked := map[string]struct{}{} 65 | 66 | for { 67 | // Keep checking until there are no file left 68 | if len(filesToCheck) == 0 { 69 | break 70 | } 71 | fileName := filesToCheck[0] 72 | 73 | // If this file has been checked before, skip 74 | if _, checked := filesAlreadyChecked[fileName]; checked { 75 | filesToCheck = filesToCheck[1:] 76 | continue 77 | } 78 | 79 | // Fetch dependencies using ldd 80 | cmdLdd := exec.Command("ldd", fileName) 81 | lddResult, err := cmdLdd.CombinedOutput() 82 | if err != nil { 83 | return fmt.Errorf("%v: %s", err, lddResult) 84 | } 85 | 86 | // Parse ldd results 87 | // It will look like this: libname.so => /path/to/lib (memory address) 88 | buffer := bytes.NewBuffer(lddResult) 89 | scanner := bufio.NewScanner(buffer) 90 | 91 | for scanner.Scan() { 92 | lib := strings.TrimSpace(scanner.Text()) 93 | libName := strings.SplitN(lib, " ", 2)[0] 94 | libName = fp.Base(libName) 95 | if libName == "" { 96 | continue 97 | } 98 | 99 | libPath := fp.Join(qtLibsDir, libName) 100 | if !fileExists(libPath) { 101 | continue 102 | } 103 | 104 | filesToCheck = append(filesToCheck, libPath) 105 | mapDependencies[libName] = libPath 106 | } 107 | 108 | // Save this files as been already checked, then move to next 109 | filesAlreadyChecked[fileName] = struct{}{} 110 | filesToCheck = filesToCheck[1:] 111 | } 112 | 113 | // Copy all dependency libs to output dir 114 | var err error 115 | dstLibsDir := fp.Join(outputDir, "libs") 116 | for libName, libPath := range mapDependencies { 117 | err = copyFile(libPath, fp.Join(dstLibsDir, libName)) 118 | if err != nil { 119 | return err 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | // createLinuxScripts creates wrapper script to run the executable 127 | func createLinuxScript(outputPath string) error { 128 | // Prepare content of the script 129 | scriptContent := "" + 130 | "#!/bin/bash\n" + 131 | "appname=`basename $0 | sed s,\\.sh$,,`\n\n" + 132 | "dirname=`dirname $0`\n" + 133 | "tmp=\"${dirname#?}\"\n\n" + 134 | "if [ \"${dirname%$tmp}\" != \"/\" ]; then\n" + 135 | "dirname=$PWD/$dirname\n" + 136 | "fi\n" + 137 | "export LD_LIBRARY_PATH=\"$dirname/libs\"\n" + 138 | "export QT_PLUGIN_PATH=\"$dirname/plugins\"\n" + 139 | "export QML_IMPORT_PATH=\"$dirname/qml\"\n" + 140 | "export QML2_IMPORT_PATH=\"$dirname/qml\"\n" + 141 | "$dirname/$appname \"$@\"\n" 142 | 143 | // Write script to file. If it already exists, remove it. 144 | scriptPath := strings.TrimSuffix(outputPath, fp.Ext(outputPath)) 145 | scriptPath += ".sh" 146 | 147 | os.Remove(scriptPath) 148 | dstFile, err := os.OpenFile(scriptPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 149 | if err != nil { 150 | return err 151 | } 152 | defer dstFile.Close() 153 | 154 | _, err = dstFile.WriteString(scriptContent) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | return dstFile.Sync() 160 | } 161 | -------------------------------------------------------------------------------- /internal/generator/deps-qml.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | fp "path/filepath" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/go-qamel/qamel/internal/config" 12 | ) 13 | 14 | var rxQmlImport = regexp.MustCompile(`^import\s+(Qt\S+)\s(\d)+.*$`) 15 | 16 | // copyQmlDependencies copies QML dependencies to output dir 17 | func copyQmlDependencies(qtQmlDir string, profile config.Profile, projectDir, outputDir string) error { 18 | // Get list of dir to check 19 | var dirToCheck []string 20 | projectResDir := fp.Join(projectDir, "res") 21 | 22 | if dirExists(projectResDir) { 23 | dirToCheck = append(dirToCheck, projectResDir) 24 | } 25 | 26 | // Get list of QML dependencies 27 | mapDependencies := map[string]struct{}{} 28 | dirAlreadyChecked := map[string]struct{}{} 29 | 30 | for { 31 | // Keep checking until there are no directory left 32 | if len(dirToCheck) == 0 { 33 | break 34 | } 35 | 36 | // If this dir has been checked before, skip 37 | dirPath := dirToCheck[0] 38 | if _, checked := dirAlreadyChecked[dirPath]; checked { 39 | dirToCheck = dirToCheck[1:] 40 | continue 41 | } 42 | 43 | // Fetch qml dependencies from the directory 44 | qmlDeps, err := getQmlDependenciesFromDir(qtQmlDir, dirPath) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | for _, qmlDep := range qmlDeps { 50 | mapDependencies[qmlDep] = struct{}{} 51 | dirToCheck = append(dirToCheck, qmlDep) 52 | } 53 | 54 | // Save this dir as been already checked, then move to next 55 | dirAlreadyChecked[dirPath] = struct{}{} 56 | dirToCheck = dirToCheck[1:] 57 | } 58 | 59 | // Copy all dependency libs to output dir 60 | dstQmlDir := outputDir 61 | if profile.OS != "windows" { 62 | dstQmlDir = fp.Join(dstQmlDir, "qml") 63 | } 64 | 65 | for qmlPath := range mapDependencies { 66 | parentExists := false 67 | qmlPathParts := fp.SplitList(qmlPath) 68 | for i := 0; i <= len(qmlPathParts)-1; i++ { 69 | parentPath := fp.Join(qmlPathParts[:i]...) 70 | if _, exist := mapDependencies[parentPath]; exist { 71 | parentExists = true 72 | break 73 | } 74 | } 75 | 76 | if parentExists { 77 | continue 78 | } 79 | 80 | dirName, err := fp.Rel(qtQmlDir, qmlPath) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = copyDir(qmlPath, fp.Join(dstQmlDir, dirName), func(path string, info os.FileInfo) bool { 86 | fileExt := fp.Ext(info.Name()) 87 | if fileExt == ".qmlc" || fileExt == ".jsc" { 88 | return true 89 | } 90 | 91 | if strings.HasSuffix(info.Name(), "d.dll") { 92 | tmpPath := strings.TrimSuffix(path, "d.dll") 93 | tmpPath += ".dll" 94 | 95 | if fileExists(tmpPath) { 96 | return true 97 | } 98 | } 99 | 100 | return false 101 | }) 102 | 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // getQmlDependenciesFromDir gets all QML imports from *.qml files 112 | // inside specified directory 113 | func getQmlDependenciesFromDir(qtQmlDir string, srcDir string) ([]string, error) { 114 | // Make sure dir existst 115 | if !dirExists(srcDir) { 116 | return nil, fmt.Errorf("directory %s doesn't exist", srcDir) 117 | } 118 | 119 | // Fetch each QML file inside the specified dir 120 | var qmlFiles []string 121 | fp.Walk(srcDir, func(path string, info os.FileInfo, err error) error { 122 | if !info.IsDir() && fp.Ext(info.Name()) == ".qml" { 123 | qmlFiles = append(qmlFiles, path) 124 | } 125 | return nil 126 | }) 127 | 128 | // Get QML dependencies from each file 129 | mapDependencies := map[string]struct{}{} 130 | for _, qmlFile := range qmlFiles { 131 | deps, err := getQmlDependenciesFromFile(qtQmlDir, qmlFile) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | for _, dep := range deps { 137 | mapDependencies[dep] = struct{}{} 138 | } 139 | } 140 | 141 | // Convert map to arrays 142 | var results []string 143 | for depName := range mapDependencies { 144 | results = append(results, depName) 145 | } 146 | 147 | return results, nil 148 | } 149 | 150 | // getQmlDependenciesFromFile gets all QML imports from *.qml files 151 | // from the specified file 152 | func getQmlDependenciesFromFile(qtQmlDir string, qmlFilePath string) ([]string, error) { 153 | // Open QML file 154 | qmlFile, err := os.Open(qmlFilePath) 155 | if err != nil { 156 | return nil, err 157 | } 158 | defer qmlFile.Close() 159 | 160 | // Read each line from the file 161 | var results []string 162 | scanner := bufio.NewScanner(qmlFile) 163 | for scanner.Scan() { 164 | // Use regex to find import line 165 | line := scanner.Text() 166 | line = strings.TrimSpace(line) 167 | matches := rxQmlImport.FindStringSubmatch(line) 168 | 169 | // If regex doesn't match, skip 170 | if len(matches) != 3 { 171 | continue 172 | } 173 | 174 | // Find possible directory for that QML import from Qt's QML dir 175 | // For example, "import QtQuick.Controls 2.11" might means the QML files located in : 176 | // - qtQmlDir/QtQuick/Controls.2 177 | // - qtQmlDir/QtQuick.2/Controls 178 | // Or the version number might be not used afterall 179 | // - qtQmlDir/QtQuick/Controls 180 | name := matches[1] 181 | nameParts := strings.Split(name, ".") 182 | 183 | possibleDir := "" 184 | version := "." + matches[2] 185 | for i := len(nameParts) - 1; i >= 0; i-- { 186 | tmp := make([]string, len(nameParts)) 187 | copy(tmp, nameParts) 188 | 189 | tmp[i] += version 190 | tmpDir := fp.Join(tmp...) 191 | tmpDir = fp.Join(qtQmlDir, tmpDir) 192 | if dirExists(tmpDir) { 193 | possibleDir = tmpDir 194 | break 195 | } 196 | } 197 | 198 | if possibleDir != "" { 199 | results = append(results, possibleDir) 200 | } else { 201 | possibleDir := fp.Join(nameParts...) 202 | possibleDir = fp.Join(qtQmlDir, possibleDir) 203 | if dirExists(possibleDir) { 204 | results = append(results, possibleDir) 205 | } 206 | } 207 | } 208 | 209 | return results, nil 210 | } 211 | -------------------------------------------------------------------------------- /internal/generator/deps-windows.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | fp "path/filepath" 10 | "runtime" 11 | "strings" 12 | 13 | "github.com/go-qamel/qamel/internal/config" 14 | ) 15 | 16 | // copyWindowsPlugins copies Windows plugins to output directory 17 | func copyWindowsPlugins(qtPluginsDir string, outputDir string) error { 18 | plugins := []string{ 19 | "platforms/qwindows.dll", 20 | "imageformats", 21 | "iconengines/qsvgicon.dll"} 22 | 23 | for _, plugin := range plugins { 24 | srcPath := fp.Join(qtPluginsDir, plugin) 25 | dstPath := fp.Join(outputDir, plugin) 26 | 27 | if !fileDirExists(srcPath) { 28 | continue 29 | } 30 | 31 | err := copyFileDir(srcPath, dstPath, func(path string, info os.FileInfo) bool { 32 | if strings.HasSuffix(info.Name(), "d.dll") { 33 | tmpPath := strings.TrimSuffix(path, "d.dll") 34 | tmpPath += ".dll" 35 | 36 | if fileExists(tmpPath) { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | }) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // copyWindowsLibs copies Windows libraries to output directory 53 | func copyWindowsLibs(profile config.Profile, outputPath string) error { 54 | // Get list of files to check. 55 | // The first one is the output binary file 56 | filesToCheck := []string{outputPath} 57 | 58 | // The plugins library (*.dll) must be checked as well 59 | outputDir := fp.Dir(outputPath) 60 | if dirExists(outputDir) { 61 | fp.Walk(outputDir, func(path string, info os.FileInfo, err error) error { 62 | if info.IsDir() { 63 | return nil 64 | } 65 | 66 | if fp.Ext(info.Name()) == ".dll" { 67 | filesToCheck = append(filesToCheck, path) 68 | } 69 | 70 | return nil 71 | }) 72 | } 73 | 74 | // Get directory of Qt and Gcc libs 75 | qtLibsDir := fp.Dir(profile.Qmake) 76 | gccLibsDir := fp.Dir(profile.Gcc) 77 | if runtime.GOOS != "windows" { 78 | // If it's not Windows, it must be using MXE, 79 | // so find GCC using relative path from qmake location 80 | gccLibsDir = fp.Join(qtLibsDir, "..", "..", "bin") 81 | } 82 | 83 | // Get list of dependency of the files 84 | mapDependencies := map[string]string{} 85 | filesAlreadyChecked := map[string]struct{}{} 86 | 87 | for { 88 | // Keep checking until there are no file left 89 | if len(filesToCheck) == 0 { 90 | break 91 | } 92 | fileName := filesToCheck[0] 93 | 94 | // If this file has been checked before, skip 95 | if _, checked := filesAlreadyChecked[fileName]; checked { 96 | filesToCheck = filesToCheck[1:] 97 | continue 98 | } 99 | 100 | // Fetch dependencies using objdump 101 | cmdObjdump := exec.Command(profile.Objdump, "-p", fileName) 102 | objdumpResult, err := cmdObjdump.CombinedOutput() 103 | if err != nil { 104 | return fmt.Errorf("%v: %s", err, objdumpResult) 105 | } 106 | 107 | // Parse objdump results 108 | buffer := bytes.NewBuffer(objdumpResult) 109 | scanner := bufio.NewScanner(buffer) 110 | 111 | for scanner.Scan() { 112 | line := strings.TrimSpace(scanner.Text()) 113 | if !strings.HasPrefix(line, "DLL Name:") { 114 | continue 115 | } 116 | 117 | libName := strings.TrimPrefix(line, "DLL Name:") 118 | libName = strings.TrimSpace(libName) 119 | if libName == "" { 120 | continue 121 | } 122 | 123 | libPath := fp.Join(qtLibsDir, libName) 124 | if !fileExists(libPath) { 125 | libPath = fp.Join(gccLibsDir, libName) 126 | if !fileExists(libPath) { 127 | continue 128 | } 129 | } 130 | 131 | filesToCheck = append(filesToCheck, libPath) 132 | mapDependencies[libName] = libPath 133 | } 134 | 135 | // Save this files as been already checked, then move to next 136 | filesAlreadyChecked[fileName] = struct{}{} 137 | filesToCheck = filesToCheck[1:] 138 | } 139 | 140 | // Copy all dependency libs to output dir 141 | var err error 142 | for libName, libPath := range mapDependencies { 143 | err = copyFile(libPath, fp.Join(outputDir, libName)) 144 | if err != nil { 145 | return err 146 | } 147 | } 148 | 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/generator/deps.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os/exec" 8 | fp "path/filepath" 9 | "strings" 10 | 11 | "github.com/go-qamel/qamel/internal/config" 12 | ) 13 | 14 | // CopyDependencies copies dependencies to output directory 15 | func CopyDependencies(profile config.Profile, projectDir, outputPath string) error { 16 | switch profile.OS { 17 | case "linux": 18 | return copyLinuxDependencies(profile, projectDir, outputPath) 19 | case "windows": 20 | return copyWindowsDependencies(profile, projectDir, outputPath) 21 | default: 22 | return nil 23 | } 24 | } 25 | 26 | // copyLinuxDependencies copies dependencies for Linux 27 | func copyLinuxDependencies(profile config.Profile, projectDir, outputPath string) error { 28 | // Get qmake variables from `qmake -query` 29 | qmakeVars, err := getQmakeVars(profile.Qmake) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Get dirs 35 | outputDir := fp.Dir(outputPath) 36 | qtQmlDir := qmakeVars["QT_INSTALL_QML"] 37 | qtLibsDir := qmakeVars["QT_INSTALL_LIBS"] 38 | qtPluginsDir := qmakeVars["QT_INSTALL_PLUGINS"] 39 | 40 | // Copy QML 41 | err = copyQmlDependencies(qtQmlDir, profile, projectDir, outputDir) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // Copy plugins 47 | err = copyLinuxPlugins(qtPluginsDir, outputDir) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // Copy libs 53 | err = copyLinuxLibs(qtLibsDir, outputPath) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Create script 59 | return createLinuxScript(outputPath) 60 | } 61 | 62 | // copyWindowsDependencies copies dependencies for Windows 63 | func copyWindowsDependencies(profile config.Profile, projectDir, outputPath string) error { 64 | // Get qmake variables from `qmake -query` 65 | qmakeVars, err := getQmakeVars(profile.Qmake) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Get dirs 71 | outputDir := fp.Dir(outputPath) 72 | qtQmlDir := qmakeVars["QT_INSTALL_QML"] 73 | qtPluginsDir := qmakeVars["QT_INSTALL_PLUGINS"] 74 | 75 | // Copy QML 76 | err = copyQmlDependencies(qtQmlDir, profile, projectDir, outputDir) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | // Copy plugins 82 | err = copyWindowsPlugins(qtPluginsDir, outputDir) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | // Copy libs 88 | return copyWindowsLibs(profile, outputPath) 89 | } 90 | 91 | // getQmakeVars get the qmake properties by running `qmake -query` 92 | func getQmakeVars(qmakePath string) (map[string]string, error) { 93 | // Run qmake 94 | cmdQmake := exec.Command(qmakePath, "-query") 95 | btOutput, err := cmdQmake.CombinedOutput() 96 | if err != nil { 97 | return nil, fmt.Errorf("%v\n%s", err, btOutput) 98 | } 99 | 100 | // Parse output 101 | buffer := bytes.NewBuffer(btOutput) 102 | scanner := bufio.NewScanner(buffer) 103 | qmakeVars := map[string]string{} 104 | for scanner.Scan() { 105 | parts := strings.SplitN(scanner.Text(), ":", 2) 106 | if len(parts) != 2 { 107 | continue 108 | } 109 | 110 | name := strings.TrimSpace(parts[0]) 111 | value := strings.TrimSpace(parts[1]) 112 | qmakeVars[name] = value 113 | } 114 | 115 | return qmakeVars, nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/generator/icon.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | fp "path/filepath" 8 | 9 | "github.com/go-qamel/qamel/internal/config" 10 | ) 11 | 12 | // ErrNoIcon is error that fired when app icon doesn't exist 13 | var ErrNoIcon = fmt.Errorf("icon file doesn't exist") 14 | 15 | // CreateSysoFile creates syso file from ICO file in the specified source. 16 | func CreateSysoFile(profile config.Profile, projectDir string) error { 17 | // Check if icon is exist 18 | iconPath := fp.Join(projectDir, "icon.ico") 19 | if !fileExists(iconPath) { 20 | return ErrNoIcon 21 | } 22 | 23 | // Create temporary rc file 24 | rcFilePath := fp.Join(projectDir, "qamel-icon.rc") 25 | defer os.Remove(rcFilePath) 26 | 27 | err := saveToFile(rcFilePath, `IDI_ICON1 ICON DISCARDABLE "icon.ico"`) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | // Create syso file 33 | sysoFilePath := fp.Join(projectDir, "qamel-icon.syso") 34 | cmdWindres := exec.Command(profile.Windres, "-i", rcFilePath, "-o", sysoFilePath) 35 | if btOutput, err := cmdWindres.CombinedOutput(); err != nil { 36 | return fmt.Errorf("%v: %s", err, btOutput) 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/generator/moc.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | fp "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // CreateMocFile creates moc file from the specified source. 11 | // If destination is not specified, the moc file will be saved in 12 | // file "moc-" + source.name + ".h" 13 | func CreateMocFile(mocPath string, src string) error { 14 | // Make sure source is exist 15 | if !fileExists(src) { 16 | return fmt.Errorf("source file doesn't exists") 17 | } 18 | 19 | // Create destination name 20 | dst := "moc-" + fp.Base(src) 21 | dst = strings.TrimSuffix(dst, fp.Ext(dst)) + ".h" 22 | dst = fp.Join(fp.Dir(src), dst) 23 | 24 | // Run moc 25 | cmdMoc := exec.Command(mocPath, "-o", dst, src) 26 | if btOutput, err := cmdMoc.CombinedOutput(); err != nil { 27 | return fmt.Errorf("%v: %s", err, btOutput) 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/generator/object-file.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/format" 6 | fp "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // createCppHeaderFile creates .h file and save it to obj.dirPath 11 | func createCppHeaderFile(obj object) error { 12 | // Create initial result 13 | result := "" 14 | 15 | // Write include guard 16 | guardName := fmt.Sprintf("QAMEL_%s_H", strings.ToUpper(obj.name)) 17 | result += fmt.Sprintf(""+ 18 | "#pragma once\n\n"+ 19 | "#ifndef %s\n"+ 20 | "#define %s\n\n", 21 | guardName, guardName) 22 | 23 | // Write std library 24 | result += "" + 25 | "#include \n" + 26 | "#include \n\n" 27 | 28 | // Write check for C++ and class declaration 29 | className := upperChar(obj.name, 0) 30 | result += fmt.Sprintf(""+ 31 | "#ifdef __cplusplus\n\n"+ 32 | "// Class\n"+ 33 | "class %s;\n\n"+ 34 | `extern "C" {`+"\n"+ 35 | "#endif\n\n", className) 36 | 37 | // Write getter and setter for properties 38 | result += fmt.Sprintln("// Properties") 39 | for i, prop := range obj.properties { 40 | propName := upperChar(prop.name, 0) 41 | propType := mapGoType[prop.memberType].inC 42 | 43 | result += fmt.Sprintf(""+ 44 | "%s %s_%s(void* ptr);\n"+ 45 | "void %s_Set%s(void* ptr, %s %s);\n", 46 | propType, className, propName, 47 | className, propName, propType, prop.name) 48 | 49 | if i < len(obj.properties)-1 { 50 | result += "\n" 51 | } 52 | } 53 | 54 | // Write method for signals 55 | result += fmt.Sprintln() 56 | result += fmt.Sprintln("// Signals") 57 | for _, signal := range obj.signals { 58 | params := []string{"void* ptr"} 59 | for _, param := range signal.parameters { 60 | paramType := mapGoType[param.memberType].inC 61 | strParam := fmt.Sprintf("%s %s", paramType, param.name) 62 | params = append(params, strParam) 63 | } 64 | 65 | signalName := upperChar(signal.name, 0) 66 | result += fmt.Sprintf("void %s_%s(%s);\n", 67 | className, signalName, strings.Join(params, ", ")) 68 | } 69 | 70 | // Write method for registering QML type 71 | result += fmt.Sprintf("\n"+ 72 | "// Register\n"+ 73 | "void %s_RegisterQML(char* uri, int versionMajor, int versionMinor, char* qmlName);\n\n", 74 | className) 75 | 76 | // Write #endifs 77 | result += fmt.Sprintln("" + 78 | "#ifdef __cplusplus\n" + 79 | "}\n" + 80 | "#endif\n\n" + 81 | "#endif") 82 | 83 | // Save result to file 84 | fileName := strings.ToLower(obj.name) 85 | fileName = fmt.Sprintf("qamel-%s.h", fileName) 86 | fileName = fp.Join(obj.dirPath, fileName) 87 | if err := saveToFile(fileName, result); err != nil { 88 | return fmt.Errorf("error creating header file for %s: %v", obj.name, err) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // createCppFile creates .cpp file and save it to obj.dirPath 95 | func createCppFile(mocPath string, obj object) error { 96 | // Create initial result 97 | result := "" 98 | 99 | // Write #include list 100 | hFileName := strings.ToLower(obj.name) 101 | hFileName = fmt.Sprintf("qamel-%s.h", hFileName) 102 | result += fmt.Sprintf(""+ 103 | "#include \n"+ 104 | "#include \n"+ 105 | "#include \n"+ 106 | "#include \n"+ 107 | "#include \n"+ 108 | "#include \n\n"+ 109 | `#include "_cgo_export.h"`+"\n"+ 110 | `#include "%s"`+"\n\n", 111 | hFileName) 112 | 113 | // Write class and property declaration 114 | className := upperChar(obj.name, 0) 115 | result += fmt.Sprintf(""+ 116 | "class %s : public QQuickItem {\n"+ 117 | "\tQ_OBJECT\n", className) 118 | 119 | for _, prop := range obj.properties { 120 | propType := mapGoType[prop.memberType].inCpp 121 | setterName := "set" + upperChar(prop.name, 0) 122 | notifierName := prop.name + "Changed" 123 | 124 | result += fmt.Sprintf("\tQ_PROPERTY(%s %s READ %s WRITE %s NOTIFY %s)\n", 125 | propType, prop.name, prop.name, setterName, notifierName) 126 | } 127 | 128 | // Write class's private member 129 | result += fmt.Sprintln("\nprivate:") 130 | for _, prop := range obj.properties { 131 | propType := mapGoType[prop.memberType].inCpp 132 | result += fmt.Sprintf("\t%s _%s;\n", propType, prop.name) 133 | } 134 | 135 | // Write class's public member 136 | result += fmt.Sprintln("\npublic:") 137 | 138 | // constructor 139 | result += fmt.Sprintf(""+ 140 | "\t%s(QQuickItem* parent=Q_NULLPTR) : QQuickItem(parent) {\n"+ 141 | "\t\tqamel%sConstructor(this);\n"+ 142 | "\t}\n\n", className, className) 143 | 144 | // destroyer 145 | result += fmt.Sprintf(""+ 146 | "\t~%s() {\n"+ 147 | "\t\tqamelDestroy%s(this);\n"+ 148 | "\t}\n\n", className, className) 149 | 150 | // getter and setter 151 | for i, prop := range obj.properties { 152 | propType := mapGoType[prop.memberType].inCpp 153 | setterName := "set" + upperChar(prop.name, 0) 154 | propNewName := "new" + upperChar(prop.name, 0) 155 | 156 | result += fmt.Sprintf(""+ 157 | "\t%s %s() { return _%s; }\n"+ 158 | "\tvoid %s(%s %s) { _%s = %s; }\n", 159 | propType, prop.name, prop.name, 160 | setterName, propType, propNewName, prop.name, propNewName) 161 | 162 | if i < len(obj.properties)-1 { 163 | result += "\n" 164 | } 165 | } 166 | 167 | // Write class's signals 168 | // properties signals 169 | result += fmt.Sprintln("signals:") 170 | for i, prop := range obj.properties { 171 | propType := mapGoType[prop.memberType].inCpp 172 | propNewName := "new" + upperChar(prop.name, 0) 173 | 174 | result += fmt.Sprintf("\tvoid %sChanged(%s %s);\n", 175 | prop.name, propType, propNewName) 176 | 177 | if i < len(obj.properties)-1 { 178 | result += "\n" 179 | } 180 | } 181 | 182 | // the real signals 183 | for i, signal := range obj.signals { 184 | var params []string 185 | for _, param := range signal.parameters { 186 | paramType := mapGoType[param.memberType].inCpp 187 | strParam := fmt.Sprintf("%s %s", paramType, param.name) 188 | params = append(params, strParam) 189 | } 190 | 191 | result += fmt.Sprintf("\tvoid %s(%s);\n", 192 | signal.name, strings.Join(params, ", ")) 193 | 194 | if i < len(obj.signals)-1 { 195 | result += "\n" 196 | } 197 | } 198 | 199 | // Write class's slots 200 | result += fmt.Sprintln("\npublic slots:") 201 | for i, slot := range obj.slots { 202 | returnType := "void" 203 | if len(slot.returns) > 0 { 204 | returnType = mapGoType[slot.returns[0].memberType].inCpp 205 | } 206 | 207 | var params []string 208 | paramNames := []string{"this"} 209 | for _, param := range slot.parameters { 210 | paramType := mapGoType[param.memberType].inCpp 211 | paramName := param.name 212 | if param.memberType == "string" { 213 | paramName = fmt.Sprintf("%s.toLocal8Bit().data()", paramName) 214 | } 215 | 216 | strParam := fmt.Sprintf("%s %s", paramType, param.name) 217 | params = append(params, strParam) 218 | paramNames = append(paramNames, paramName) 219 | } 220 | 221 | result += fmt.Sprintf("\t%s %s(%s) {\n", 222 | returnType, slot.name, strings.Join(params, ", ")) 223 | 224 | result += "\t\t" 225 | if returnType != "void" { 226 | result += "return " 227 | } 228 | 229 | result += fmt.Sprintf("qamel%s%s(%s);\n\t}\n", 230 | className, upperChar(slot.name, 0), 231 | strings.Join(paramNames, ", ")) 232 | 233 | if i < len(obj.slots)-1 { 234 | result += "\n" 235 | } 236 | } 237 | 238 | // Finished writing definition of class 239 | result += "};\n" 240 | 241 | // Write public methods 242 | // for manipulating properties 243 | for i, prop := range obj.properties { 244 | propName := upperChar(prop.name, 0) 245 | propType := mapGoType[prop.memberType].inCpp 246 | propHeaderType := mapGoType[prop.memberType].inC 247 | 248 | // getter 249 | result += fmt.Sprintf(""+ 250 | "%s %s_%s(void* ptr) {\n"+ 251 | "\t%s *obj = static_cast<%s*>(ptr);\n"+ 252 | "\treturn obj->%s()", 253 | propHeaderType, className, propName, 254 | className, className, 255 | prop.name) 256 | if prop.memberType == "string" { 257 | result += ".toLocal8Bit().data();\n" 258 | } else { 259 | result += ";\n" 260 | } 261 | result += "}\n\n" 262 | 263 | // setter 264 | result += fmt.Sprintf(""+ 265 | "void %s_Set%s(void* ptr, %s %s) {\n"+ 266 | "\t%s *obj = static_cast<%s*>(ptr);\n"+ 267 | "\tobj->set%s(%s(%s));\n"+ 268 | "}\n", 269 | className, propName, propHeaderType, prop.name, 270 | className, className, 271 | propName, propType, prop.name) 272 | 273 | if i < len(obj.properties)-1 { 274 | result += "\n" 275 | } 276 | } 277 | 278 | // for invoking signals 279 | result += "\n" 280 | for i, signal := range obj.signals { 281 | var invokerParams []string 282 | params := []string{"void* ptr"} 283 | for _, param := range signal.parameters { 284 | paramType := mapGoType[param.memberType].inCpp 285 | paramHeaderType := mapGoType[param.memberType].inC 286 | 287 | strParam := fmt.Sprintf("%s %s", paramHeaderType, param.name) 288 | strInvokerParam := fmt.Sprintf("%s(%s)", paramType, param.name) 289 | 290 | params = append(params, strParam) 291 | invokerParams = append(invokerParams, strInvokerParam) 292 | } 293 | 294 | signalName := upperChar(signal.name, 0) 295 | result += fmt.Sprintf(""+ 296 | "void %s_%s(%s) {\n"+ 297 | "\t%s *obj = static_cast<%s*>(ptr);\n"+ 298 | "\tobj->%s(%s);\n"+ 299 | "}\n", className, signalName, strings.Join(params, ", "), 300 | className, className, 301 | signal.name, strings.Join(invokerParams, ", ")) 302 | 303 | if i < len(obj.signals)-1 { 304 | result += "\n" 305 | } 306 | } 307 | 308 | // for registering QML 309 | result += fmt.Sprintf("\n"+ 310 | "void %s_RegisterQML(char* uri, int versionMajor, int versionMinor, char* qmlName) {\n"+ 311 | "\tqmlRegisterType<%s>(uri, versionMajor, versionMinor, qmlName);\n"+ 312 | "}\n", className, className) 313 | 314 | // Write #include moc file 315 | mocFileName := fmt.Sprintf("moc-%s", hFileName) 316 | result += fmt.Sprintf("\n"+`#include "%s"`+"\n", mocFileName) 317 | 318 | // Save result to file 319 | cppFileName := strings.ToLower(obj.name) 320 | cppFileName = fmt.Sprintf("qamel-%s.cpp", cppFileName) 321 | cppFileName = fp.Join(obj.dirPath, cppFileName) 322 | if err := saveToFile(cppFileName, result); err != nil { 323 | return fmt.Errorf("error creating C++ file for %s: %v", obj.name, err) 324 | } 325 | 326 | // Create moc file 327 | err := CreateMocFile(mocPath, cppFileName) 328 | if err != nil { 329 | return fmt.Errorf("error creating moc file for %s: %v", obj.name, err) 330 | } 331 | 332 | return nil 333 | } 334 | 335 | // createGoFile creates .go file and save it to obj.dirPath 336 | func createGoFile(obj object) error { 337 | // Create file name 338 | baseName := strings.ToLower(obj.name) 339 | hFileName := fmt.Sprintf("qamel-%s.h", baseName) 340 | 341 | // Create initial result 342 | result := fmt.Sprintf("package %s\n", obj.packageName) 343 | result += fmt.Sprintln() 344 | 345 | // Write clause for importing C packages 346 | result += fmt.Sprintf(""+ 347 | "// #include \n"+ 348 | "// #include \n"+ 349 | "// #include \n"+ 350 | "// #include \n"+ 351 | "// #include \"%s\"\n"+ 352 | `import "C"`+"\n", hFileName) 353 | 354 | // Write clause for importing Go packages 355 | result += "" + 356 | "import (\n" + 357 | `"unsafe"` + "\n" + 358 | `"github.com/go-qamel/qamel"` + "\n" + 359 | ")\n" 360 | 361 | // Write function for C constructor 362 | cClassName := upperChar(obj.name, 0) 363 | result += fmt.Sprintf(""+ 364 | "//export qamel%sConstructor\n"+ 365 | "func qamel%sConstructor(ptr unsafe.Pointer) {\n"+ 366 | "obj := &%s{}\n"+ 367 | "obj.Ptr = ptr\n"+ 368 | "qamel.RegisterObject(ptr, obj)\n", 369 | cClassName, cClassName, obj.name) 370 | 371 | if len(obj.constructors) == 1 { 372 | result += fmt.Sprintf("obj.%s()\n", obj.constructors[0].name) 373 | } 374 | 375 | result += "}\n\n" 376 | 377 | // Write function for C destroyer 378 | result += fmt.Sprintf(""+ 379 | "//export qamelDestroy%s\n"+ 380 | "func qamelDestroy%s(ptr unsafe.Pointer) {\n"+ 381 | "qamel.DeleteObject(ptr)\n"+ 382 | "}\n\n", cClassName, cClassName) 383 | 384 | // Write function for C slots 385 | for _, slot := range obj.slots { 386 | returnType := "" 387 | cgoReturnType := "" 388 | if len(slot.returns) > 0 { 389 | returnType = slot.returns[0].memberType 390 | cgoReturnType = fmt.Sprintf("(result %s)", mapGoType[returnType].inCgo) 391 | } 392 | 393 | var castedNames []string 394 | var castedParams []string 395 | params := []string{"ptr unsafe.Pointer"} 396 | for _, param := range slot.parameters { 397 | cgoType := mapGoType[param.memberType].inCgo 398 | strParam := fmt.Sprintf("%s %s", param.name, cgoType) 399 | castedName := fmt.Sprintf("cgo%s", upperChar(param.name, 0)) 400 | 401 | params = append(params, strParam) 402 | castedNames = append(castedNames, castedName) 403 | castedParams = append(castedParams, fmt.Sprintf("%s := %s", 404 | castedName, mapGoType[param.memberType].cgo2Go(param.name))) 405 | } 406 | 407 | slotName := upperChar(slot.name, 0) 408 | result += fmt.Sprintf(""+ 409 | "//export qamel%s%s\n"+ 410 | "func qamel%s%s(%s) %s {\n"+ 411 | "obj := qamel.BorrowObject(ptr)\n"+ 412 | "defer qamel.ReturnObject(ptr)\n"+ 413 | "if obj == nil {\n"+ 414 | "return\n"+ 415 | "}\n\n"+ 416 | "obj%s, ok := obj.(*%s)\n"+ 417 | "if !ok {\n"+ 418 | "return\n"+ 419 | "}\n\n"+ 420 | "%s\n", 421 | cClassName, slotName, cClassName, slotName, 422 | strings.Join(params, ", "), cgoReturnType, 423 | cClassName, obj.name, 424 | strings.Join(castedParams, "\n")) 425 | 426 | returnValue := fmt.Sprintf("obj%s.%s(%s)", 427 | cClassName, slot.name, strings.Join(castedNames, ", ")) 428 | if returnType != "" { 429 | result += fmt.Sprintf("result = %s\n", mapGoType[returnType].go2C(returnValue)) 430 | } else { 431 | result += returnValue + "\n" 432 | } 433 | result += "return\n}\n\n" 434 | } 435 | 436 | // Write struct member function 437 | // for manipulating properties 438 | result += "// getter and setter\n\n" 439 | for _, prop := range obj.properties { 440 | propName := upperChar(prop.name, 0) 441 | 442 | // getter 443 | result += fmt.Sprintf(""+ 444 | "func (obj *%s) %s() (propValue %s) {\n"+ 445 | "if obj.Ptr == nil || !qamel.ObjectExists(obj.Ptr) {\n"+ 446 | "return\n"+ 447 | "}\n\n"+ 448 | "c%s := C.%s_%s(obj.Ptr)\n", 449 | obj.name, prop.name, prop.memberType, 450 | propName, cClassName, propName) 451 | 452 | result += fmt.Sprintf("propValue = %s\nreturn\n}\n\n", 453 | mapGoType[prop.memberType].cgo2Go("c"+propName)) 454 | 455 | // setter 456 | result += fmt.Sprintf(""+ 457 | "func (obj *%s) set%s(new%s %s) {\n"+ 458 | "if obj.Ptr == nil || !qamel.ObjectExists(obj.Ptr) {\n"+ 459 | "return\n"+ 460 | "}\n\n"+ 461 | "cNew%s := %s\n", 462 | obj.name, propName, propName, prop.memberType, 463 | propName, mapGoType[prop.memberType].go2C("new"+propName)) 464 | 465 | if prop.memberType == "string" { 466 | result += fmt.Sprintf("defer C.free(unsafe.Pointer(cNew%s))\n", propName) 467 | } 468 | 469 | result += fmt.Sprintf("C.%s_Set%s(obj.Ptr, cNew%s)\n}\n\n", 470 | cClassName, propName, propName) 471 | } 472 | 473 | // for invoking signals 474 | result += "// signals invoker\n\n" 475 | for _, signal := range obj.signals { 476 | var params []string 477 | var castedParams []string 478 | castedNames := []string{"obj.Ptr"} 479 | for _, param := range signal.parameters { 480 | strParam := fmt.Sprintf("%s %s", param.name, param.memberType) 481 | castedName := fmt.Sprintf("c%s", upperChar(param.name, 0)) 482 | 483 | params = append(params, strParam) 484 | castedNames = append(castedNames, castedName) 485 | castedParams = append(castedParams, fmt.Sprintf("%s := %s", 486 | castedName, mapGoType[param.memberType].go2C(param.name))) 487 | 488 | if param.memberType == "string" { 489 | castedParams = append(castedParams, 490 | fmt.Sprintf("defer C.free(unsafe.Pointer(%s))", castedName)) 491 | } 492 | } 493 | 494 | result += fmt.Sprintf(""+ 495 | "func (obj *%s) %s(%s) {\n"+ 496 | "if obj.Ptr == nil || !qamel.ObjectExists(obj.Ptr) {\n"+ 497 | "return\n"+ 498 | "}\n\n"+ 499 | "%s\n"+ 500 | "C.%s_%s(%s)}\n\n", 501 | obj.name, signal.name, strings.Join(params, ", "), 502 | strings.Join(castedParams, "\n"), 503 | cClassName, upperChar(signal.name, 0), 504 | strings.Join(castedNames, ", ")) 505 | } 506 | 507 | // Write function for registering QML object 508 | result += fmt.Sprintf(""+ 509 | "// RegisterQml%s registers %s as QML object\n"+ 510 | "func RegisterQml%s(uri string, versionMajor int, versionMinor int, qmlName string) {\n"+ 511 | "cURI := C.CString(uri)\n"+ 512 | "cQmlName := C.CString(qmlName)\n"+ 513 | "cVersionMajor := C.int(int32(versionMajor))\n"+ 514 | "cVersionMinor := C.int(int32(versionMinor))\n"+ 515 | "defer func() {\n"+ 516 | "C.free(unsafe.Pointer(cURI))\n"+ 517 | "C.free(unsafe.Pointer(cQmlName))\n"+ 518 | "}()\n\n"+ 519 | "C.%s_RegisterQML(cURI, cVersionMajor, cVersionMinor, cQmlName)\n"+ 520 | "}\n\n", cClassName, obj.name, cClassName, cClassName) 521 | 522 | // Format code 523 | fmtResult, err := format.Source([]byte(result)) 524 | if err != nil { 525 | return fmt.Errorf("error formatting Go file for %s: %v", obj.name, err) 526 | } 527 | 528 | // Save result to file 529 | fileName := strings.ToLower(obj.name) 530 | fileName = fmt.Sprintf("qamel-%s.go", fileName) 531 | fileName = fp.Join(obj.dirPath, fileName) 532 | if err = saveToFile(fileName, string(fmtResult)); err != nil { 533 | return fmt.Errorf("error creating Go file for %s: %v", obj.name, err) 534 | } 535 | 536 | return nil 537 | } 538 | -------------------------------------------------------------------------------- /internal/generator/object-type.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import "fmt" 4 | 5 | type goTypeConverter struct { 6 | inC string 7 | inCpp string 8 | inCgo string 9 | cgo2Go func(name string) string 10 | go2C func(name string) string 11 | } 12 | 13 | var mapGoType = map[string]goTypeConverter{ 14 | "int": goTypeConverter{ 15 | inC: "int", 16 | inCpp: "int", 17 | inCgo: "C.int", 18 | cgo2Go: func(name string) string { 19 | return fmt.Sprintf("int(int32(%s))", name) 20 | }, 21 | go2C: func(name string) string { 22 | return fmt.Sprintf("C.int(int32(%s))", name) 23 | }, 24 | }, 25 | 26 | "int32": goTypeConverter{ 27 | inC: "int", 28 | inCpp: "int", 29 | inCgo: "C.int", 30 | cgo2Go: func(name string) string { 31 | return fmt.Sprintf("int32(%s)", name) 32 | }, 33 | go2C: func(name string) string { 34 | return fmt.Sprintf("C.int(%s)", name) 35 | }, 36 | }, 37 | 38 | "int64": goTypeConverter{ 39 | inC: "long", 40 | inCpp: "long", 41 | inCgo: "C.long", 42 | cgo2Go: func(name string) string { 43 | return fmt.Sprintf("int64(%s)", name) 44 | }, 45 | go2C: func(name string) string { 46 | return fmt.Sprintf("C.long(%s)", name) 47 | }, 48 | }, 49 | 50 | "float32": goTypeConverter{ 51 | inC: "float", 52 | inCpp: "float", 53 | inCgo: "C.float", 54 | cgo2Go: func(name string) string { 55 | return fmt.Sprintf("float32(%s)", name) 56 | }, 57 | go2C: func(name string) string { 58 | return fmt.Sprintf("C.float(%s)", name) 59 | }, 60 | }, 61 | 62 | "float64": goTypeConverter{ 63 | inC: "double", 64 | inCpp: "double", 65 | inCgo: "C.double", 66 | cgo2Go: func(name string) string { 67 | return fmt.Sprintf("float64(%s)", name) 68 | }, 69 | go2C: func(name string) string { 70 | return fmt.Sprintf("C.double(%s)", name) 71 | }, 72 | }, 73 | 74 | "bool": goTypeConverter{ 75 | inC: "bool", 76 | inCpp: "bool", 77 | inCgo: "C.bool", 78 | cgo2Go: func(name string) string { 79 | return fmt.Sprintf("bool(%s)", name) 80 | }, 81 | go2C: func(name string) string { 82 | return fmt.Sprintf("C.bool(%s)", name) 83 | }, 84 | }, 85 | 86 | "string": goTypeConverter{ 87 | inC: "char*", 88 | inCpp: "QString", 89 | inCgo: "*C.char", 90 | cgo2Go: func(name string) string { 91 | return fmt.Sprintf("C.GoString(%s)", name) 92 | }, 93 | go2C: func(name string) string { 94 | return fmt.Sprintf("C.CString(%s)", name) 95 | }, 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /internal/generator/object.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/build" 7 | "go/parser" 8 | "go/token" 9 | fp "path/filepath" 10 | "reflect" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/go-qamel/qamel/internal/config" 15 | ) 16 | 17 | var ( 18 | qamelImportName = "qamel" 19 | qamelImportPath = "github.com/go-qamel/qamel" 20 | qamelObjectName = "QmlObject" 21 | 22 | rxNumber = regexp.MustCompile(`\d`) 23 | rxSymbol = regexp.MustCompile(`[^A-Za-z0-9]`) 24 | ) 25 | 26 | type object struct { 27 | name string 28 | dirPath string 29 | fileName string 30 | packageName string 31 | structNode *ast.StructType 32 | constructors []objectMethod 33 | properties []objectMember 34 | signals []objectMethod 35 | slots []objectMethod 36 | } 37 | 38 | type objectMember struct { 39 | name string 40 | memberType string 41 | } 42 | 43 | type objectMethod struct { 44 | name string 45 | parameters []objectMember 46 | returns []objectMember 47 | } 48 | 49 | // CreateQmlObjectCode generates Go code and C++ code for all QmlObject 50 | // in specified directory 51 | func CreateQmlObjectCode(profile config.Profile, projectDir string, buildTags ...string) []error { 52 | // Make sure project directory is exists 53 | if !dirExists(projectDir) { 54 | err := fmt.Errorf("directory %s doesn't exist", projectDir) 55 | return []error{err} 56 | } 57 | 58 | // Find all sub directories, including the root dir 59 | subDirs, err := getSubDirs(projectDir) 60 | if err != nil { 61 | return []error{err} 62 | } 63 | 64 | // For each sub directories, find Go files with matching build tags 65 | var goFiles []string 66 | buildCtx := build.Default 67 | buildCtx.BuildTags = append([]string{}, buildTags...) 68 | for _, dir := range subDirs { 69 | // If a directory doesn't have any Go files, this method will throw error. 70 | // That's why in this part, when error happened just continue. 71 | dirPkg, err := buildCtx.ImportDir(dir, 0) 72 | if err != nil { 73 | continue 74 | } 75 | 76 | for _, goFile := range dirPkg.GoFiles { 77 | goFiles = append(goFiles, fp.Join(dir, goFile)) 78 | } 79 | } 80 | 81 | // From each Go files, find struct with qamel.QmlObject embedded to it 82 | var qmlObjects []object 83 | for _, goFile := range goFiles { 84 | objects, err := getQmlObjectStructs(goFile) 85 | if err != nil { 86 | return []error{err} 87 | } 88 | qmlObjects = append(qmlObjects, objects...) 89 | } 90 | 91 | // Parse each qml objects 92 | var errors []error 93 | for i, obj := range qmlObjects { 94 | tmpObj, tmpErrors := parseQmlObject(obj) 95 | errors = append(errors, tmpErrors...) 96 | qmlObjects[i] = tmpObj 97 | } 98 | 99 | if len(errors) > 0 { 100 | return errors 101 | } 102 | 103 | // Create code for each QML objects 104 | mapDirPackage := map[string]string{} 105 | for _, obj := range qmlObjects { 106 | mapDirPackage[obj.dirPath] = obj.packageName 107 | 108 | err = createCppHeaderFile(obj) 109 | if err != nil { 110 | return []error{err} 111 | } 112 | 113 | err = createCppFile(profile.Moc, obj) 114 | if err != nil { 115 | return []error{err} 116 | } 117 | 118 | err = createGoFile(obj) 119 | if err != nil { 120 | return []error{err} 121 | } 122 | } 123 | 124 | // Create cgo file for each package 125 | for dir, packageName := range mapDirPackage { 126 | err = CreateCgoFile(profile, dir, packageName) 127 | if err != nil { 128 | return []error{err} 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | 135 | // getQmlObjectStruct fetch structs that embedded with qamel.QmlObject inside specified Go file 136 | func getQmlObjectStructs(goFile string) ([]object, error) { 137 | // Parse file 138 | fset := token.NewFileSet() 139 | f, err := parser.ParseFile(fset, goFile, nil, 0) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | // Check if this file importing qamel package 145 | usingQamel := false 146 | importName := qamelImportName 147 | for _, importSpec := range f.Imports { 148 | importPath := strings.Trim(importSpec.Path.Value, `"`) 149 | if importPath != qamelImportPath { 150 | continue 151 | } 152 | 153 | if importSpec.Name != nil { 154 | importName = importSpec.Name.Name 155 | } 156 | 157 | usingQamel = true 158 | } 159 | 160 | if !usingQamel { 161 | return []object{}, nil 162 | } 163 | 164 | // Traverse file tree to find struct that embedding QmlObject 165 | var result []object 166 | embeddedName := fmt.Sprintf("%s.%s", importName, qamelObjectName) 167 | ast.Inspect(f, func(node ast.Node) bool { 168 | // Make sure this node is a declaration of `type` 169 | typeNode, isTypeSpec := node.(*ast.TypeSpec) 170 | if !isTypeSpec { 171 | return true 172 | } 173 | 174 | // Make sure this type is declaring a struct 175 | structNode, isStruct := typeNode.Type.(*ast.StructType) 176 | if !isStruct { 177 | return false 178 | } 179 | 180 | // Make sure this struct has fields 181 | if structNode.Fields == nil || len(structNode.Fields.List) == 0 { 182 | return false 183 | } 184 | 185 | // Check this struck embedding QmlObject 186 | embeddingQamel := false 187 | for _, field := range structNode.Fields.List { 188 | selector, isSelector := field.Type.(*ast.SelectorExpr) 189 | if !isSelector { 190 | continue 191 | } 192 | 193 | selectorValue := fmt.Sprintf("%s.%s", selector.X, selector.Sel.Name) 194 | if selectorValue == embeddedName { 195 | embeddingQamel = true 196 | break 197 | } 198 | } 199 | 200 | if !embeddingQamel { 201 | return false 202 | } 203 | 204 | // Save this struct to list of object 205 | result = append(result, object{ 206 | name: typeNode.Name.Name, 207 | dirPath: fp.Dir(goFile), 208 | fileName: goFile, 209 | packageName: f.Name.Name, 210 | structNode: structNode, 211 | }) 212 | 213 | return false 214 | }) 215 | 216 | return result, nil 217 | } 218 | 219 | // parseNode parse struct nodes inside object and find the property, signal and slots 220 | func parseQmlObject(obj object) (object, []error) { 221 | var ( 222 | errors []error 223 | slots []objectMethod 224 | signals []objectMethod 225 | properties []objectMember 226 | constructors []objectMethod 227 | ) 228 | 229 | nPropName := map[string]int{} 230 | nSignalName := map[string]int{} 231 | nSlotName := map[string]int{} 232 | 233 | for _, structField := range obj.structNode.Fields.List { 234 | // Make sure this field either identity or function 235 | identField, isIdent := structField.Type.(*ast.Ident) 236 | funcField, isFunc := structField.Type.(*ast.FuncType) 237 | if !isIdent && !isFunc { 238 | continue 239 | } 240 | 241 | // Get and check field tag 242 | if structField.Tag == nil { 243 | continue 244 | } 245 | 246 | fieldTag := strings.Trim(structField.Tag.Value, "`") 247 | structTag := reflect.StructTag(fieldTag) 248 | 249 | propName := strings.TrimSpace(structTag.Get("property")) 250 | signalName := strings.TrimSpace(structTag.Get("signal")) 251 | slotName := strings.TrimSpace(structTag.Get("slot")) 252 | constructorName := strings.TrimSpace(structTag.Get("constructor")) 253 | mergedName := propName + signalName + slotName + constructorName 254 | 255 | if mergedName == "" { 256 | continue 257 | } 258 | 259 | if mergedName != propName && mergedName != signalName && mergedName != slotName && mergedName != constructorName { 260 | err := fmt.Errorf("object %s: a field must be only used for one purpose", obj.name) 261 | errors = append(errors, err) 262 | continue 263 | } 264 | 265 | // Check whether this field is blank or not 266 | isBlankField := len(structField.Names) == 1 && structField.Names[0].String() == "_" 267 | 268 | // Check if it's property 269 | if isIdent && propName != "" { 270 | if !isBlankField { 271 | err := fmt.Errorf("object %s, property %s: must be a single blank field", obj.name, propName) 272 | errors = append(errors, err) 273 | continue 274 | } 275 | 276 | if err := validateTagName(propName); err != nil { 277 | err = fmt.Errorf("object %s, property %s: %v", obj.name, propName, err) 278 | errors = append(errors, err) 279 | continue 280 | } 281 | 282 | if nPropName[propName] > 0 { 283 | err := fmt.Errorf("object %s, property %s: property has been declared before", obj.name, propName) 284 | errors = append(errors, err) 285 | continue 286 | } 287 | 288 | properties = append(properties, objectMember{ 289 | name: propName, 290 | memberType: identField.String(), 291 | }) 292 | 293 | nPropName[propName]++ 294 | continue 295 | } 296 | 297 | // Check if it's constructor 298 | if isFunc && constructorName != "" { 299 | if !isBlankField { 300 | err := fmt.Errorf("object %s, constructor %s: must be a single blank field", obj.name, constructorName) 301 | errors = append(errors, err) 302 | continue 303 | } 304 | 305 | if err := validateTagName(constructorName); err != nil { 306 | err = fmt.Errorf("object %s, constructor %s: %v", obj.name, constructorName, err) 307 | errors = append(errors, err) 308 | continue 309 | } 310 | 311 | if len(constructors) > 0 { 312 | err := fmt.Errorf("object %s, constructor %s: other constructor has been declared before", obj.name, constructorName) 313 | errors = append(errors, err) 314 | continue 315 | } 316 | 317 | if funcField.Results != nil { 318 | err := fmt.Errorf("object %s, constructor %s: must not have return value", obj.name, constructorName) 319 | errors = append(errors, err) 320 | continue 321 | } 322 | 323 | parameters := parseAstFuncParams(funcField.Params) 324 | if len(parameters) > 0 { 325 | err := fmt.Errorf("object %s, constructor %s: must not have any parameter", obj.name, constructorName) 326 | errors = append(errors, err) 327 | continue 328 | } 329 | 330 | constructors = append(constructors, objectMethod{ 331 | name: constructorName, 332 | }) 333 | 334 | continue 335 | } 336 | 337 | // Check if it's signal 338 | if isFunc && signalName != "" { 339 | if !isBlankField { 340 | err := fmt.Errorf("object %s, signal %s: must be a single blank field", obj.name, signalName) 341 | errors = append(errors, err) 342 | continue 343 | } 344 | 345 | if err := validateTagName(signalName); err != nil { 346 | err = fmt.Errorf("object %s, signal %s: %v", obj.name, signalName, err) 347 | errors = append(errors, err) 348 | continue 349 | } 350 | 351 | if nSignalName[signalName] > 0 { 352 | err := fmt.Errorf("object %s, signal %s: signal has been declared before", obj.name, signalName) 353 | errors = append(errors, err) 354 | continue 355 | } 356 | 357 | if funcField.Results != nil { 358 | err := fmt.Errorf("object %s, signal %s: must not have return value", obj.name, signalName) 359 | errors = append(errors, err) 360 | continue 361 | } 362 | 363 | signalParameters := parseAstFuncParams(funcField.Params) 364 | err := validateMethodType(signalParameters) 365 | if err != nil { 366 | err1 := fmt.Errorf("object %s, signal %s: %v", obj.name, signalName, err) 367 | errors = append(errors, err1) 368 | continue 369 | } 370 | 371 | signals = append(signals, objectMethod{ 372 | name: signalName, 373 | parameters: signalParameters, 374 | }) 375 | 376 | nSignalName[signalName]++ 377 | continue 378 | } 379 | 380 | // Check if it's slot 381 | if isFunc && slotName != "" { 382 | if !isBlankField { 383 | err := fmt.Errorf("object %s, slot %s: must be a single blank field", obj.name, slotName) 384 | errors = append(errors, err) 385 | continue 386 | } 387 | 388 | if err := validateTagName(slotName); err != nil { 389 | err = fmt.Errorf("object %s, slot %s: %v", obj.name, slotName, err) 390 | errors = append(errors, err) 391 | continue 392 | } 393 | 394 | if nSlotName[slotName] > 0 { 395 | err := fmt.Errorf("object %s, slot %s: slot has been declared before", obj.name, slotName) 396 | errors = append(errors, err) 397 | continue 398 | } 399 | 400 | slotReturns := parseAstFuncParams(funcField.Results) 401 | if len(slotReturns) > 1 { 402 | err := fmt.Errorf("object %s, slot %s: only allowed max one return value", obj.name, slotName) 403 | errors = append(errors, err) 404 | continue 405 | } 406 | 407 | err := validateMethodType(slotReturns) 408 | if err != nil { 409 | err1 := fmt.Errorf("object %s, slot %s: %v", obj.name, slotName, err) 410 | errors = append(errors, err1) 411 | continue 412 | } 413 | 414 | slotParameters := parseAstFuncParams(funcField.Params) 415 | err = validateMethodType(slotParameters) 416 | if err != nil { 417 | err1 := fmt.Errorf("object %s, slot %s: %v", obj.name, slotName, err) 418 | errors = append(errors, err1) 419 | continue 420 | } 421 | 422 | slots = append(slots, objectMethod{ 423 | name: slotName, 424 | parameters: slotParameters, 425 | returns: slotReturns, 426 | }) 427 | 428 | nSlotName[slotName]++ 429 | } 430 | } 431 | 432 | if len(errors) == 0 { 433 | obj.slots = slots 434 | obj.signals = signals 435 | obj.properties = properties 436 | obj.constructors = constructors 437 | } 438 | 439 | return obj, errors 440 | } 441 | 442 | // parseAstFuncParams converts field list to object member 443 | func parseAstFuncParams(fieldList *ast.FieldList) []objectMember { 444 | // Make sure field list exists 445 | if fieldList == nil { 446 | return []objectMember{} 447 | } 448 | 449 | // Get name and type of each parameter 450 | paramIdx := 0 451 | var result []objectMember 452 | for _, param := range fieldList.List { 453 | if len(param.Names) == 0 { 454 | result = append(result, objectMember{ 455 | name: fmt.Sprintf("p%d", paramIdx), 456 | memberType: fmt.Sprint(param.Type), 457 | }) 458 | paramIdx++ 459 | continue 460 | } 461 | 462 | for _, nameIdent := range param.Names { 463 | result = append(result, objectMember{ 464 | name: nameIdent.String(), 465 | memberType: fmt.Sprint(param.Type), 466 | }) 467 | paramIdx++ 468 | } 469 | } 470 | 471 | return result 472 | } 473 | 474 | // validateMethodType check if member has unknown type 475 | func validateMethodType(members []objectMember) error { 476 | for _, member := range members { 477 | if _, known := mapGoType[member.memberType]; !known { 478 | return fmt.Errorf("unknown type %s", member.memberType) 479 | } 480 | } 481 | 482 | return nil 483 | } 484 | 485 | // validateTagName check if member has a valid tag name 486 | func validateTagName(tagName string) error { 487 | if tagName == "" { 488 | return nil 489 | } 490 | 491 | firstChar := tagName[0:1] 492 | if rxNumber.MatchString(firstChar) { 493 | return fmt.Errorf("name must not started with number") 494 | } 495 | 496 | if firstChar == strings.ToUpper(firstChar) { 497 | return fmt.Errorf("name must be unexported") 498 | } 499 | 500 | if rxSymbol.MatchString(tagName) { 501 | return fmt.Errorf("name must be only consisted of letters and numbers") 502 | } 503 | 504 | return nil 505 | } 506 | -------------------------------------------------------------------------------- /internal/generator/rcc.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | fp "path/filepath" 8 | 9 | "github.com/go-qamel/qamel/internal/config" 10 | ) 11 | 12 | // ErrNoResourceDir is error that fired when resource directory doesn't exist 13 | var ErrNoResourceDir = fmt.Errorf("resource directory doesn't exist") 14 | 15 | // CreateRccFile creates rcc.cpp file from resource directory at `projectDir/res` 16 | func CreateRccFile(profile config.Profile, projectDir string) error { 17 | // Create cgo file 18 | err := CreateCgoFile(profile, projectDir, "main") 19 | if err != nil { 20 | return err 21 | } 22 | 23 | // Check if resource directory is exist 24 | resDir := fp.Join(projectDir, "res") 25 | if !dirExists(resDir) { 26 | return ErrNoResourceDir 27 | } 28 | 29 | // Get list of file inside resource dir 30 | var resFiles []string 31 | fp.Walk(resDir, func(path string, info os.FileInfo, err error) error { 32 | if info.IsDir() { 33 | return nil 34 | } 35 | 36 | path, _ = fp.Rel(projectDir, path) 37 | resFiles = append(resFiles, path) 38 | return nil 39 | }) 40 | 41 | if len(resFiles) == 0 { 42 | return fmt.Errorf("no resource available") 43 | } 44 | 45 | // Create temp qrc file 46 | qrcPath := fp.Join(projectDir, "qamel.qrc") 47 | defer os.Remove(qrcPath) 48 | 49 | qrcContent := fmt.Sprintln(``) 50 | qrcContent += fmt.Sprintln(``) 51 | for _, resFile := range resFiles { 52 | qrcContent += fmt.Sprintf("%s\n", resFile) 53 | } 54 | qrcContent += fmt.Sprintln(``) 55 | qrcContent += fmt.Sprintln(``) 56 | 57 | err = saveToFile(qrcPath, qrcContent) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // Run rcc 63 | dst := fp.Join(projectDir, "qamel-rcc.cpp") 64 | cmdRcc := exec.Command(profile.Rcc, "-o", dst, qrcPath) 65 | btOutput, err := cmdRcc.CombinedOutput() 66 | if err != nil { 67 | return fmt.Errorf("%v\n%s", err, btOutput) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/generator/utils.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "go/token" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | fp "path/filepath" 11 | "strings" 12 | "unicode" 13 | ) 14 | 15 | // fileExists checks if the file in specified path is exists 16 | func fileExists(filePath string) bool { 17 | info, err := os.Stat(filePath) 18 | return !os.IsNotExist(err) && !info.IsDir() 19 | } 20 | 21 | // dirExists checks if the directory in specified path is exists 22 | func dirExists(dirPath string) bool { 23 | info, err := os.Stat(dirPath) 24 | return !os.IsNotExist(err) && info.IsDir() 25 | } 26 | 27 | // fileDirExists checks if the file or dir in specified path is exists 28 | func fileDirExists(filePath string) bool { 29 | _, err := os.Stat(filePath) 30 | return !os.IsNotExist(err) 31 | } 32 | 33 | // dirEmpty checks if the directory in specified path is EMPTY 34 | func dirEmpty(dirPath string) bool { 35 | f, err := os.Open(dirPath) 36 | if err != nil { 37 | return false 38 | } 39 | defer f.Close() 40 | 41 | _, err = f.Readdirnames(1) // Or f.Readdir(1) 42 | if err == io.EOF { 43 | return true 44 | } 45 | 46 | return false 47 | } 48 | 49 | // getPackageName gets the package name from specified file 50 | func getPackageName(filePath string) (string, error) { 51 | fset := token.NewFileSet() 52 | f, err := parser.ParseFile(fset, filePath, nil, parser.PackageClauseOnly) 53 | if err != nil { 54 | return "", fmt.Errorf("failed to get package name: %s", err) 55 | } 56 | 57 | if f.Name == nil { 58 | return "", fmt.Errorf("failed to get package name: no package name found") 59 | } 60 | 61 | return f.Name.Name, nil 62 | } 63 | 64 | // getPackageNameFromDir gets the package name from specified directory 65 | func getPackageNameFromDir(dirPath string) (string, error) { 66 | dirItems, err := ioutil.ReadDir(dirPath) 67 | if err != nil { 68 | return "", fmt.Errorf("failed to get package name: %s", err) 69 | } 70 | 71 | fileName := "" 72 | for _, item := range dirItems { 73 | if !item.IsDir() && fp.Ext(item.Name()) == ".go" { 74 | fileName = fp.Join(dirPath, item.Name()) 75 | break 76 | } 77 | } 78 | 79 | if fileName == "" { 80 | return "", fmt.Errorf("failed to get package name: no go file exists in directory") 81 | } 82 | 83 | return getPackageName(fileName) 84 | } 85 | 86 | // getSubDirs fetch all sub directories, including the root dir 87 | func getSubDirs(rootDir string) ([]string, error) { 88 | subDirs := []string{} 89 | err := fp.Walk(rootDir, func(path string, info os.FileInfo, err error) error { 90 | if !info.IsDir() { 91 | return nil 92 | } 93 | 94 | if strings.HasPrefix(info.Name(), ".") { 95 | return fp.SkipDir 96 | } 97 | 98 | subDirs = append(subDirs, path) 99 | return nil 100 | }) 101 | 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return subDirs, nil 107 | } 108 | 109 | // upperChar change char in pos to uppercase. 110 | func upperChar(str string, pos int) string { 111 | if pos < 0 || pos >= len(str) { 112 | return str 113 | } 114 | 115 | tmp := []byte(str) 116 | upper := unicode.ToUpper(rune(tmp[pos])) 117 | tmp[pos] = byte(upper) 118 | return string(tmp) 119 | } 120 | 121 | // saveToFile saves a string content to file 122 | func saveToFile(dstPath string, content string) error { 123 | dstFile, err := os.Create(dstPath) 124 | if err != nil { 125 | return err 126 | } 127 | defer dstFile.Close() 128 | 129 | _, err = dstFile.WriteString(content) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | return dstFile.Sync() 135 | } 136 | 137 | // copyFile copies file from srcPath to dstPath 138 | func copyFile(srcPath, dstPath string) error { 139 | os.MkdirAll(fp.Dir(dstPath), os.ModePerm) 140 | 141 | src, err := os.Open(srcPath) 142 | if err != nil { 143 | return err 144 | } 145 | defer src.Close() 146 | 147 | dst, err := os.Create(dstPath) 148 | if err != nil { 149 | return err 150 | } 151 | defer dst.Close() 152 | 153 | _, err = io.Copy(dst, src) 154 | return err 155 | } 156 | 157 | // copyDir copies dir from srcPath to dstPath 158 | func copyDir(srcPath, dstPath string, isSkipped func(string, os.FileInfo) bool) error { 159 | os.MkdirAll(fp.Dir(dstPath), os.ModePerm) 160 | 161 | fds, err := ioutil.ReadDir(srcPath) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | for _, fd := range fds { 167 | srcfp := fp.Join(srcPath, fd.Name()) 168 | dstfp := fp.Join(dstPath, fd.Name()) 169 | 170 | if isSkipped != nil && isSkipped(srcfp, fd) { 171 | continue 172 | } 173 | 174 | if fd.IsDir() { 175 | err = copyDir(srcfp, dstfp, isSkipped) 176 | if err != nil { 177 | return err 178 | } 179 | } else { 180 | err = copyFile(srcfp, dstfp) 181 | if err != nil { 182 | return err 183 | } 184 | } 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // copyFileDir is wrapper for both copyFile and copyDir. 191 | // Useful when you don't care whether src is file or directory 192 | func copyFileDir(srcPath, dstPath string, isSkipped func(string, os.FileInfo) bool) error { 193 | stat, err := os.Stat(srcPath) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | if stat.IsDir() { 199 | return copyDir(srcPath, dstPath, isSkipped) 200 | } 201 | 202 | return copyFile(srcPath, dstPath) 203 | } 204 | -------------------------------------------------------------------------------- /listmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "_cgo_export.h" 2 | #include "listmodel.h" 3 | 4 | // Overrided method for QAbstractListModel 5 | int QamelListModel::rowCount(const QModelIndex &) const { 6 | return _contents.size(); 7 | } 8 | 9 | QVariant QamelListModel::data(const QModelIndex &index, int role) const { 10 | if (role != Qt::DisplayRole) { 11 | return QVariant(); 12 | } 13 | 14 | return _contents.at(index.row()); 15 | } 16 | 17 | QHash QamelListModel::roleNames() const { 18 | QHash role; 19 | role[Qt::DisplayRole] = "display"; 20 | return role; 21 | } 22 | 23 | bool QamelListModel::insertRows(int row, int count, const QModelIndex &parent) { 24 | return QAbstractListModel::insertRows(row, count, parent); 25 | } 26 | 27 | bool QamelListModel::removeRows(int row, int count, const QModelIndex &parent) { 28 | return QAbstractListModel::removeRows(row, count, parent); 29 | } 30 | 31 | bool QamelListModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) { 32 | return QAbstractListModel::moveRows(sourceParent, sourceRow, count, destinationParent, destinationChild); 33 | } 34 | 35 | // Methods for custom QML properties 36 | QVariantList QamelListModel::contents() const { 37 | return _contents; 38 | } 39 | 40 | void QamelListModel::setContents(QVariantList contents) { 41 | beginResetModel(); 42 | _contents = contents; 43 | endResetModel(); 44 | 45 | emit contentsChanged(); 46 | } 47 | 48 | // Methods for custom slots 49 | QVariant QamelListModel::get(int row) { 50 | return _contents.at(row); 51 | } 52 | 53 | int QamelListModel::count() { 54 | return _contents.count(); 55 | } 56 | 57 | void QamelListModel::clear() { 58 | beginResetModel(); 59 | _contents.clear(); 60 | endResetModel(); 61 | } 62 | 63 | void QamelListModel::insertRow(int row, QVariant obj) { 64 | beginInsertRows(QModelIndex(), row, row); 65 | _contents.insert(row, obj); 66 | endInsertRows(); 67 | 68 | emit contentsChanged(); 69 | } 70 | 71 | void QamelListModel::insertRows(int row, QVariantList objects) { 72 | beginInsertRows(QModelIndex(), row, row+objects.count()-1); 73 | for (int i = 0; i < objects.count(); ++i) { 74 | _contents.insert(row+i, objects[i]); 75 | } 76 | endInsertRows(); 77 | 78 | emit contentsChanged(); 79 | } 80 | 81 | void QamelListModel::appendRow(QVariant obj) { 82 | int row = _contents.count(); 83 | 84 | beginInsertRows(QModelIndex(), row, row); 85 | _contents.insert(row, obj); 86 | endInsertRows(); 87 | 88 | emit contentsChanged(); 89 | } 90 | 91 | void QamelListModel::appendRows(QVariantList objects) { 92 | int row = _contents.count(); 93 | int last = row + objects.count() - 1; 94 | 95 | beginInsertRows(QModelIndex(), row, last); 96 | for (int i = 0; i < objects.count(); ++i) { 97 | _contents.insert(row+i, objects[i]); 98 | } 99 | endInsertRows(); 100 | 101 | emit contentsChanged(); 102 | } 103 | 104 | void QamelListModel::deleteRows(int row, int count) { 105 | if (row < 0 || row >= rowCount()) return; 106 | 107 | int last = row+count-1; 108 | 109 | beginRemoveRows(QModelIndex(), row, last); 110 | for (int i = last; i >= row; --i) { 111 | _contents.removeAt(i); 112 | } 113 | endRemoveRows(); 114 | 115 | emit contentsChanged(); 116 | } 117 | 118 | void QamelListModel::setRow(int row, QVariantMap newObj) { 119 | if (row < 0 || row >= rowCount()) return; 120 | 121 | _contents[row] = newObj; 122 | emit contentsChanged(); 123 | emit dataChanged(createIndex(row, 0), createIndex(row, 0)); 124 | } 125 | 126 | void QamelListModel::setRowProperty(int row, QString propName, QVariant newProp) { 127 | if (row < 0 || row >= rowCount()) return; 128 | 129 | QVariant obj = _contents.at(row); 130 | QVariantMap map = obj.toMap(); 131 | map[propName] = newProp; 132 | _contents[row] = QVariant(map); 133 | 134 | emit contentsChanged(); 135 | emit dataChanged(createIndex(row, 0), createIndex(row, 0)); 136 | } 137 | 138 | void QamelListModel::swapRow(int i, int j) { 139 | if (i == j) return; 140 | if (i < 0 || i >= rowCount()) return; 141 | if (j < 0 || j >= rowCount()) return; 142 | 143 | _contents.swap(i, j); 144 | emit dataChanged(createIndex(i, 0), createIndex(i, 0)); 145 | emit dataChanged(createIndex(j, 0), createIndex(j, 0)); 146 | } 147 | 148 | void QamelListModel::moveRows(int first, int last, int dst) { 149 | int lastDst = dst + (last - first); 150 | 151 | if (first < 0 || first >= rowCount()) return; 152 | if (last < 0 || last >= rowCount()) return; 153 | if (dst< 0 || dst >= rowCount()) return; 154 | if (lastDst < 0 || lastDst >= rowCount()) return; 155 | 156 | beginMoveRows(QModelIndex(), first, last, QModelIndex(), dst); 157 | for (int i = last; i >= first; --i) { 158 | _contents.move(i, lastDst - (last - i)); 159 | } 160 | endMoveRows(); 161 | 162 | emit contentsChanged(); 163 | } 164 | 165 | void QamelListModel_RegisterQML(char* uri, int versionMajor, int versionMinor, char* qmlName) { 166 | qmlRegisterType(uri, versionMajor, versionMinor, qmlName); 167 | } 168 | 169 | #include "moc-listmodel.h" -------------------------------------------------------------------------------- /listmodel.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include "listmodel.h" 7 | import "C" 8 | import "unsafe" 9 | 10 | // RegisterQmlListModel registers QamelListModel as QML object 11 | func RegisterQmlListModel(uri string, versionMajor int, versionMinor int, qmlName string) { 12 | cURI := C.CString(uri) 13 | cQmlName := C.CString(qmlName) 14 | cVersionMajor := C.int(int32(versionMajor)) 15 | cVersionMinor := C.int(int32(versionMinor)) 16 | defer func() { 17 | C.free(unsafe.Pointer(cURI)) 18 | C.free(unsafe.Pointer(cQmlName)) 19 | }() 20 | 21 | C.QamelListModel_RegisterQML(cURI, cVersionMajor, cVersionMinor, cQmlName) 22 | } 23 | -------------------------------------------------------------------------------- /listmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef QAMEL_TABLEMODEL_H 4 | #define QAMEL_TABLEMODEL_H 5 | 6 | #ifdef __cplusplus 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class QamelListModel : public QAbstractListModel 16 | { 17 | Q_OBJECT 18 | Q_PROPERTY(QVariantList contents READ contents WRITE setContents NOTIFY contentsChanged) 19 | 20 | public: 21 | int rowCount(const QModelIndex & = QModelIndex()) const override; 22 | QVariant data(const QModelIndex &index, int role) const override; 23 | QHash roleNames() const override; 24 | bool insertRows(int, int, const QModelIndex & = QModelIndex()) override; 25 | bool removeRows(int, int, const QModelIndex & = QModelIndex()) override; 26 | bool moveRows(const QModelIndex &, int, int, const QModelIndex &, int) override; 27 | 28 | QVariantList contents() const; 29 | 30 | public slots: 31 | void setContents(QVariantList); 32 | 33 | QVariant get(int); 34 | int count(); 35 | void clear(); 36 | void insertRow(int, QVariant); 37 | void insertRows(int, QVariantList); 38 | void appendRow(QVariant); 39 | void appendRows(QVariantList); 40 | void deleteRows(int, int); 41 | void setRow(int, QVariantMap); 42 | void setRowProperty(int, QString, QVariant); 43 | void swapRow(int, int); 44 | void moveRows(int, int, int); 45 | 46 | signals: 47 | void contentsChanged(); 48 | 49 | private: 50 | QVariantList _contents; 51 | }; 52 | 53 | extern "C" { 54 | #endif // __cplusplus 55 | 56 | // Register QML 57 | void QamelListModel_RegisterQML(char* uri, int versionMajor, int versionMinor, char* qmlName); 58 | 59 | #ifdef __cplusplus 60 | } 61 | #endif 62 | 63 | #endif // QAMEL_TABLEMODEL_H 64 | -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | import ( 4 | "sync" 5 | "unsafe" 6 | ) 7 | 8 | var ( 9 | mutex = sync.Mutex{} 10 | mapObject = map[unsafe.Pointer]interface{}{} 11 | ) 12 | 13 | // QmlObject is the base of QML object 14 | type QmlObject struct { 15 | Ptr unsafe.Pointer 16 | } 17 | 18 | // RegisterObject registers the specified pointer to specified object 19 | func RegisterObject(ptr unsafe.Pointer, obj interface{}) { 20 | if ptr == nil || obj == nil { 21 | return 22 | } 23 | 24 | mutex.Lock() 25 | mapObject[ptr] = obj 26 | mutex.Unlock() 27 | } 28 | 29 | // BorrowObject fetch object for the specified pointer 30 | func BorrowObject(ptr unsafe.Pointer) interface{} { 31 | if ptr == nil { 32 | return nil 33 | } 34 | 35 | mutex.Lock() 36 | return mapObject[ptr] 37 | } 38 | 39 | // ReturnObject returns pointer and lock map again 40 | func ReturnObject(ptr unsafe.Pointer) { 41 | if ptr == nil { 42 | return 43 | } 44 | 45 | mutex.Unlock() 46 | } 47 | 48 | // ObjectExists checks if object exists in map 49 | func ObjectExists(ptr unsafe.Pointer) bool { 50 | obj, ok := mapObject[ptr] 51 | return ok && obj != nil 52 | } 53 | 54 | // DeleteObject remove object for the specified pointer 55 | func DeleteObject(ptr unsafe.Pointer) { 56 | if ptr == nil { 57 | return 58 | } 59 | 60 | mutex.Lock() 61 | delete(mapObject, ptr) 62 | mutex.Unlock() 63 | } 64 | -------------------------------------------------------------------------------- /quickstyle.cpp: -------------------------------------------------------------------------------- 1 | #include "quickstyle.h" 2 | #include 3 | #include 4 | 5 | void SetQuickStyle(char* style) { 6 | QQuickStyle::setStyle(QString(style)); 7 | } 8 | 9 | void SetQuickStyleFallback(char* style) { 10 | QQuickStyle::setFallbackStyle(QString(style)); 11 | } 12 | 13 | void AddQuickStylePath(char* style) { 14 | QQuickStyle::addStylePath(QString(style)); 15 | } 16 | -------------------------------------------------------------------------------- /quickstyle.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // #include 4 | // #include "quickstyle.h" 5 | import "C" 6 | import "unsafe" 7 | 8 | // SetQuickStyle sets the application style to style. 9 | // Note that the style must be configured before loading QML that 10 | // imports Qt Quick Controls 2. It is not possible to change 11 | // the style after the QML types have been registered. 12 | func SetQuickStyle(style string) { 13 | cStyle := C.CString(style) 14 | defer C.free(unsafe.Pointer(cStyle)) 15 | C.SetQuickStyle(cStyle) 16 | } 17 | 18 | // SetQuickStyleFallback sets the application fallback style 19 | // to style. Note that the fallback style must be the name of 20 | // one of the built-in Qt Quick Controls 2 styles, e.g. "Material". 21 | func SetQuickStyleFallback(style string) { 22 | cStyle := C.CString(style) 23 | defer C.free(unsafe.Pointer(cStyle)) 24 | C.SetQuickStyleFallback(cStyle) 25 | } 26 | 27 | // AddQuickStylePath adds path as a directory where Qt Quick 28 | // Controls 2 searches for available styles. The path may be 29 | // any local filesystem directory or Qt Resource directory. 30 | func AddQuickStylePath(path string) { 31 | cPath := C.CString(path) 32 | defer C.free(unsafe.Pointer(cPath)) 33 | C.AddQuickStylePath(cPath) 34 | } 35 | -------------------------------------------------------------------------------- /quickstyle.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef QAMEL_QUICKSTYLE_H 4 | #define QAMEL_QUICKSTYLE_H 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | void SetQuickStyle(char* style); 11 | void SetQuickStyleFallback(char* style); 12 | void AddQuickStylePath(char* style); 13 | 14 | #ifdef __cplusplus 15 | } 16 | #endif 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /tablemodel.cpp: -------------------------------------------------------------------------------- 1 | #include "_cgo_export.h" 2 | #include "tablemodel.h" 3 | 4 | // Overrided method for QAbstractTableModel 5 | int QamelTableModel::columnCount(const QModelIndex &) const { 6 | return _columns; 7 | } 8 | 9 | int QamelTableModel::rowCount(const QModelIndex &) const { 10 | return _contents.size(); 11 | } 12 | 13 | QVariant QamelTableModel::data(const QModelIndex &index, int role) const { 14 | if (role != Qt::DisplayRole) { 15 | return QVariant(); 16 | } 17 | 18 | QString colName = getColumnName(index.column()); 19 | QVariant obj = _contents.at(index.row()); 20 | return obj.toMap().value(colName); 21 | } 22 | 23 | QHash QamelTableModel::roleNames() const { 24 | QHash role; 25 | role[Qt::DisplayRole] = "display"; 26 | return role; 27 | } 28 | 29 | bool QamelTableModel::insertRows(int row, int count, const QModelIndex &parent) { 30 | return QAbstractTableModel::insertRows(row, count, parent); 31 | } 32 | 33 | bool QamelTableModel::removeRows(int row, int count, const QModelIndex &parent) { 34 | return QAbstractTableModel::removeRows(row, count, parent); 35 | } 36 | 37 | bool QamelTableModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) { 38 | return QAbstractTableModel::moveRows(sourceParent, sourceRow, count, destinationParent, destinationChild); 39 | } 40 | 41 | // Methods for custom QML properties 42 | int QamelTableModel::columns() const { 43 | return _columns; 44 | } 45 | 46 | QVariantList QamelTableModel::contents() const { 47 | return _contents; 48 | } 49 | 50 | QVariantMap QamelTableModel::columnsName() const { 51 | return _columnsName; 52 | } 53 | 54 | void QamelTableModel::setColumns(int columns) { 55 | _columns = columns; 56 | emit columnsChanged(); 57 | emit layoutChanged(); 58 | } 59 | 60 | void QamelTableModel::setContents(QVariantList contents) { 61 | beginResetModel(); 62 | _contents = contents; 63 | endResetModel(); 64 | 65 | emit contentsChanged(); 66 | } 67 | 68 | void QamelTableModel::setColumnsName(QVariantMap columnsName) { 69 | _columnsName = columnsName; 70 | emit columnsNameChanged(); 71 | emit layoutChanged(); 72 | } 73 | 74 | // Private method 75 | QString QamelTableModel::getColumnName(int column) const { 76 | QVariant val = _columnsName[QString::number(column)]; 77 | return val.toString(); 78 | } 79 | 80 | // Methods for custom slots 81 | QVariant QamelTableModel::get(int row) { 82 | return _contents.at(row); 83 | } 84 | 85 | int QamelTableModel::count() { 86 | return _contents.count(); 87 | } 88 | 89 | void QamelTableModel::clear() { 90 | beginResetModel(); 91 | _contents.clear(); 92 | endResetModel(); 93 | } 94 | 95 | void QamelTableModel::insertRow(int row, QVariant obj) { 96 | beginInsertRows(QModelIndex(), row, row); 97 | _contents.insert(row, obj); 98 | endInsertRows(); 99 | 100 | emit contentsChanged(); 101 | } 102 | 103 | void QamelTableModel::insertRows(int row, QVariantList objects) { 104 | beginInsertRows(QModelIndex(), row, row+objects.count()-1); 105 | for (int i = 0; i < objects.count(); ++i) { 106 | _contents.insert(row+i, objects[i]); 107 | } 108 | endInsertRows(); 109 | 110 | emit contentsChanged(); 111 | } 112 | 113 | void QamelTableModel::appendRow(QVariant obj) { 114 | int row = _contents.count(); 115 | 116 | beginInsertRows(QModelIndex(), row, row); 117 | _contents.insert(row, obj); 118 | endInsertRows(); 119 | 120 | emit contentsChanged(); 121 | } 122 | 123 | void QamelTableModel::appendRows(QVariantList objects) { 124 | int row = _contents.count(); 125 | int last = row + objects.count() - 1; 126 | 127 | beginInsertRows(QModelIndex(), row, last); 128 | for (int i = 0; i < objects.count(); ++i) { 129 | _contents.insert(row+i, objects[i]); 130 | } 131 | endInsertRows(); 132 | 133 | emit contentsChanged(); 134 | } 135 | 136 | void QamelTableModel::deleteRows(int row, int count) { 137 | if (row < 0 || row >= rowCount()) return; 138 | 139 | int last = row+count-1; 140 | 141 | beginRemoveRows(QModelIndex(), row, last); 142 | for (int i = last; i >= row; --i) { 143 | _contents.removeAt(i); 144 | } 145 | endRemoveRows(); 146 | 147 | emit contentsChanged(); 148 | } 149 | 150 | void QamelTableModel::setRow(int row, QVariantMap newObj) { 151 | if (row < 0 || row >= rowCount()) return; 152 | 153 | _contents[row] = newObj; 154 | emit contentsChanged(); 155 | emit dataChanged(createIndex(row, 0), createIndex(row, columnCount()-1)); 156 | } 157 | 158 | void QamelTableModel::setRowProperty(int row, QString propName, QVariant newProp) { 159 | if (row < 0 || row >= rowCount()) return; 160 | 161 | QVariant obj = _contents.at(row); 162 | QVariantMap map = obj.toMap(); 163 | map[propName] = newProp; 164 | _contents[row] = QVariant(map); 165 | 166 | emit contentsChanged(); 167 | emit dataChanged(createIndex(row, 0), createIndex(row, columnCount()-1)); 168 | } 169 | 170 | void QamelTableModel::swapRow(int i, int j) { 171 | if (i == j) return; 172 | if (i < 0 || i >= rowCount()) return; 173 | if (j < 0 || j >= rowCount()) return; 174 | 175 | _contents.swap(i, j); 176 | emit dataChanged(createIndex(i, 0), createIndex(i, columnCount()-1)); 177 | emit dataChanged(createIndex(j, 0), createIndex(j, columnCount()-1)); 178 | } 179 | 180 | void QamelTableModel::moveRows(int first, int last, int dst) { 181 | int lastDst = dst + (last - first); 182 | 183 | if (first < 0 || first >= rowCount()) return; 184 | if (last < 0 || last >= rowCount()) return; 185 | if (dst< 0 || dst >= rowCount()) return; 186 | if (lastDst < 0 || lastDst >= rowCount()) return; 187 | 188 | beginMoveRows(QModelIndex(), first, last, QModelIndex(), dst); 189 | for (int i = last; i >= first; --i) { 190 | _contents.move(i, lastDst - (last - i)); 191 | } 192 | endMoveRows(); 193 | 194 | emit contentsChanged(); 195 | } 196 | 197 | void QamelTableModel_RegisterQML(char* uri, int versionMajor, int versionMinor, char* qmlName) { 198 | qmlRegisterType(uri, versionMajor, versionMinor, qmlName); 199 | } 200 | 201 | #include "moc-tablemodel.h" -------------------------------------------------------------------------------- /tablemodel.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include "tablemodel.h" 7 | import "C" 8 | import "unsafe" 9 | 10 | // RegisterQmlTableModel registers QamelTableModel as QML object 11 | func RegisterQmlTableModel(uri string, versionMajor int, versionMinor int, qmlName string) { 12 | cURI := C.CString(uri) 13 | cQmlName := C.CString(qmlName) 14 | cVersionMajor := C.int(int32(versionMajor)) 15 | cVersionMinor := C.int(int32(versionMinor)) 16 | defer func() { 17 | C.free(unsafe.Pointer(cURI)) 18 | C.free(unsafe.Pointer(cQmlName)) 19 | }() 20 | 21 | C.QamelTableModel_RegisterQML(cURI, cVersionMajor, cVersionMinor, cQmlName) 22 | } 23 | -------------------------------------------------------------------------------- /tablemodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef QAMEL_TABLEMODEL_H 4 | #define QAMEL_TABLEMODEL_H 5 | 6 | #ifdef __cplusplus 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class QamelTableModel : public QAbstractTableModel 16 | { 17 | Q_OBJECT 18 | Q_PROPERTY(int columns READ columns WRITE setColumns NOTIFY columnsChanged) 19 | Q_PROPERTY(QVariantList contents READ contents WRITE setContents NOTIFY contentsChanged) 20 | Q_PROPERTY(QVariantMap columnsName READ columnsName WRITE setColumnsName NOTIFY columnsNameChanged) 21 | 22 | public: 23 | int rowCount(const QModelIndex & = QModelIndex()) const override; 24 | int columnCount(const QModelIndex & = QModelIndex()) const override; 25 | QVariant data(const QModelIndex &index, int role) const override; 26 | QHash roleNames() const override; 27 | bool insertRows(int, int, const QModelIndex & = QModelIndex()) override; 28 | bool removeRows(int, int, const QModelIndex & = QModelIndex()) override; 29 | bool moveRows(const QModelIndex &, int, int, const QModelIndex &, int) override; 30 | 31 | int columns() const; 32 | QVariantList contents() const; 33 | QVariantMap columnsName() const; 34 | 35 | public slots: 36 | void setColumns(int); 37 | void setContents(QVariantList); 38 | void setColumnsName(QVariantMap columnsName); 39 | 40 | QVariant get(int); 41 | int count(); 42 | void clear(); 43 | void insertRow(int, QVariant); 44 | void insertRows(int, QVariantList); 45 | void appendRow(QVariant); 46 | void appendRows(QVariantList); 47 | void deleteRows(int, int); 48 | void setRow(int, QVariantMap); 49 | void setRowProperty(int, QString, QVariant); 50 | void swapRow(int, int); 51 | void moveRows(int, int, int); 52 | 53 | signals: 54 | void columnsChanged(); 55 | void contentsChanged(); 56 | void columnsNameChanged(); 57 | 58 | private: 59 | int _columns; 60 | QVariantList _contents; 61 | QVariantMap _columnsName; 62 | 63 | QString getColumnName(int) const; 64 | }; 65 | 66 | extern "C" { 67 | #endif // __cplusplus 68 | 69 | // Register QML 70 | void QamelTableModel_RegisterQML(char* uri, int versionMajor, int versionMinor, char* qmlName); 71 | 72 | #ifdef __cplusplus 73 | } 74 | #endif 75 | 76 | #endif // QAMEL_TABLEMODEL_H 77 | -------------------------------------------------------------------------------- /viewer-enum.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // ResizeMode specifies how to resize the view. 4 | type ResizeMode int32 5 | 6 | const ( 7 | // SizeViewToRootObject makes the view resizes with the root item in the QML. 8 | SizeViewToRootObject ResizeMode = 0 9 | 10 | // SizeRootObjectToView makes the view will automatically resize 11 | // the root item to the size of the view. 12 | SizeRootObjectToView = 1 13 | ) 14 | 15 | // WindowFlags is used to specify various window-system properties for the widget. 16 | // They are fairly unusual but necessary in a few cases. Some of these flags depend on 17 | // whether the underlying window manager supports them. 18 | type WindowFlags int32 19 | 20 | const ( 21 | // Widget is the default type for QWidget. Widgets of this type are child widgets 22 | // if they have a parent, and independent windows if they have no parent. 23 | Widget WindowFlags = 0x00000000 24 | 25 | // Window indicates that the widget is a window, usually with a window system frame and 26 | // a title bar, irrespective of whether the widget has a parent or not. Note that it is 27 | // not possible to unset this flag if the widget does not have a parent. 28 | Window = 0x00000001 29 | 30 | // Dialog indicates that the widget is a window that should be decorated as a dialog 31 | // (i.e., typically no maximize or minimize buttons in the title bar). This is the default 32 | // type for QDialog. If you want to use it as a modal dialog, it should be launched from 33 | // another window, or have a parent and used with the QWidget::windowModality property. 34 | // If you make it modal, the dialog will prevent other top-level windows in the application 35 | // from getting any input. We refer to a top-level window that has a parent as a secondary window. 36 | Dialog = 0x00000002 | Window 37 | 38 | // Sheet indicates that the window is a sheet on macOS. Since using a sheet implies window 39 | // modality, the recommended way is to use QWidget::setWindowModality(), or QDialog::open(), instead. 40 | Sheet = 0x00000004 | Window 41 | 42 | // Drawer indicates that the widget is a drawer on macOS. 43 | Drawer = Sheet | Dialog 44 | 45 | // Popup indicates that the widget is a pop-up top-level window, i.e. that it is modal, but has 46 | // a window system frame appropriate for pop-up menus. 47 | Popup = 0x00000008 | Window 48 | 49 | // Tool indicates that the widget is a tool window. A tool window is often a small window with 50 | // a smaller than usual title bar and decoration, typically used for collections of tool buttons. 51 | // If there is a parent, the tool window will always be kept on top of it. If there isn't a 52 | // parent, you may consider using WindowStaysOnTopHint as well. If the window system supports it, 53 | // a tool window can be decorated with a somewhat lighter frame. It can also be combined with 54 | // FramelessWindowHint. On macOS, tool windows correspond to the NSPanel class of windows. This 55 | // means that the window lives on a level above normal windows making it impossible to put a normal 56 | // window on top of it. By default, tool windows will disappear when the application is inactive. 57 | // This can be controlled by the WA_MacAlwaysShowToolWindow attribute. 58 | Tool = Popup | Dialog 59 | 60 | // ToolTip indicates that the widget is a tooltip. This is used internally to implement tooltips. 61 | ToolTip = Popup | Sheet 62 | 63 | // SplashScreen indicates that the window is a splash screen. This is the default type for QSplashScreen. 64 | SplashScreen = ToolTip | Dialog 65 | 66 | // Desktop indicates that this widget is the desktop. This is the type for QDesktopWidget. 67 | Desktop = 0x00000010 | Window 68 | 69 | // SubWindow indicates that this widget is a sub-window, such as a QMdiSubWindow widget. 70 | SubWindow = 0x00000012 71 | 72 | // ForeignWindow indicates that this window object is a handle representing a native platform 73 | // window created by another process or by manually using native code. 74 | ForeignWindow = 0x00000020 | Window 75 | 76 | // CoverWindow indicates that the window represents a cover window, which is shown when the 77 | // application is minimized on some platforms. 78 | CoverWindow = 0x00000040 | Window 79 | 80 | // MSWindowsFixedSizeDialogHint gives the window a thin dialog border on Windows. This style is 81 | // traditionally used for fixed-size dialogs. 82 | MSWindowsFixedSizeDialogHint = 0x00000100 83 | 84 | // MSWindowsOwnDC gives the window its own display context on Windows. 85 | MSWindowsOwnDC = 0x00000200 86 | 87 | // BypassWindowManagerHint can be used to indicate to the platform plugin that "all" 88 | // window manager protocols should be disabled. This flag will behave different depending on what 89 | // operating system the application is running on and what window manager is running. The flag 90 | // can be used to get a native window with no configuration set. 91 | BypassWindowManagerHint = 0x00000400 92 | 93 | // X11BypassWindowManagerHint bypass the window manager completely. This results in a borderless 94 | // window that is not managed at all (i.e., no keyboard input unless you call 95 | // QWidget::activateWindow() manually). 96 | X11BypassWindowManagerHint = BypassWindowManagerHint 97 | 98 | // FramelessWindowHint produces a borderless window. The user cannot move or resize a borderless 99 | // window via the window system. On X11, the result of the flag is dependent on the window manager 100 | // and its ability to understand Motif and/or NETWM hints. Most existing modern window managers 101 | // can handle this. 102 | FramelessWindowHint = 0x00000800 103 | 104 | // NoDropShadowWindowHint disables window drop shadow on supporting platforms. 105 | NoDropShadowWindowHint = 0x40000000 106 | 107 | // CustomizeWindowHint turns off the default window title hints. 108 | CustomizeWindowHint = 0x02000000 109 | 110 | // WindowTitleHint gives the window a title bar. 111 | WindowTitleHint = 0x00001000 112 | 113 | // WindowSystemMenuHint adds a window system menu, and possibly a close button (for example 114 | // on Mac). If you need to hide or show a close button, it is more portable to use WindowCloseButtonHint. 115 | WindowSystemMenuHint = 0x00002000 116 | 117 | // WindowMinimizeButtonHint adds a minimize button. On some platforms this implies 118 | // WindowSystemMenuHint for it to work. 119 | WindowMinimizeButtonHint = 0x00004000 120 | 121 | // WindowMaximizeButtonHint adds a maximize button. On some platforms this implies 122 | // WindowSystemMenuHint for it to work. 123 | WindowMaximizeButtonHint = 0x00008000 124 | 125 | // WindowMinMaxButtonsHint adds a minimize and a maximize button. On some platforms 126 | // this implies WindowSystemMenuHint for it to work. 127 | WindowMinMaxButtonsHint = WindowMinimizeButtonHint | WindowMaximizeButtonHint 128 | 129 | // WindowCloseButtonHint adds a close button. On some platforms this implies 130 | // WindowSystemMenuHint for it to work. 131 | WindowCloseButtonHint = 0x08000000 132 | 133 | // WindowContextHelpButtonHint adds a context help button to dialogs. On some platforms this 134 | // implies WindowSystemMenuHint for it to work. 135 | WindowContextHelpButtonHint = 0x00010000 136 | 137 | // MacWindowToolBarButtonHint on macOS adds a tool bar button (i.e., the oblong button that is 138 | // on the top right of windows that have toolbars). 139 | MacWindowToolBarButtonHint = 0x10000000 140 | 141 | // WindowFullscreenButtonHint on macOS adds a fullscreen button. 142 | WindowFullscreenButtonHint = 0x80000000 143 | 144 | // BypassGraphicsProxyWidget prevents the window and its children from automatically embedding 145 | // themselves into a QGraphicsProxyWidget if the parent widget is already embedded. You can set 146 | // this flag if you want your widget to always be a toplevel widget on the desktop, regardless 147 | // of whether the parent widget is embedded in a scene or not. 148 | BypassGraphicsProxyWidget = 0x20000000 149 | 150 | // WindowShadeButtonHint adds a shade button in place of the minimize button if the underlying 151 | // window manager supports it. 152 | WindowShadeButtonHint = 0x00020000 153 | 154 | // WindowStaysOnTopHint informs the window system that the window should stay on top of all 155 | // other windows. Note that on some window managers on X11 you also have to pass 156 | // X11BypassWindowManagerHint for this flag to work correctly. 157 | WindowStaysOnTopHint = 0x00040000 158 | 159 | // WindowStaysOnBottomHint informs the window system that the window should stay on bottom of 160 | // all other windows. Note that on X11 this hint will work only in window managers that support 161 | // _NET_WM_STATE_BELOW atom. If a window always on the bottom has a parent, the parent will 162 | // also be left on the bottom. This window hint is currently not implemented for macOS. 163 | WindowStaysOnBottomHint = 0x04000000 164 | 165 | // WindowTransparentForInput informs the window system that this window is used only for 166 | // output (displaying something) and does not take input. Therefore input events should 167 | // pass through as if it wasn't there. 168 | WindowTransparentForInput = 0x00080000 169 | 170 | // WindowOverridesSystemGestures informs the window system that this window implements its 171 | // own set of gestures and that system level gestures, like for instance three-finger desktop 172 | // switching, should be disabled. 173 | WindowOverridesSystemGestures = 0x00100000 174 | 175 | // WindowDoesNotAcceptFocus informs the window system that this window should not 176 | // receive the input focus. 177 | WindowDoesNotAcceptFocus = 0x00200000 178 | 179 | // MaximizeUsingFullscreenGeometryHint informs the window system that when maximizing the 180 | // window it should use as much of the available screen geometry as possible, including areas 181 | // that may be covered by system UI such as status bars or application launchers. This may 182 | // result in the window being placed under these system UIs, but does not guarantee it, 183 | // depending on whether or not the platform supports it. When the flag is enabled the user 184 | // is responsible for taking QScreen::availableGeometry() into account, so that any UI elements 185 | // in the application that require user interaction are not covered by system UI. 186 | MaximizeUsingFullscreenGeometryHint = 0x00400000 187 | ) 188 | 189 | // WindowStates is used to specify the current state of a top-level window. 190 | type WindowStates int32 191 | 192 | const ( 193 | // WindowNoState makes the window has no state set (in normal state). 194 | WindowNoState WindowStates = 0x00000000 195 | 196 | // WindowMinimized makes the window is minimized (i.e. iconified). 197 | WindowMinimized = 0x00000001 198 | 199 | // WindowMaximized makes the window is maximized with a frame around it. 200 | WindowMaximized = 0x00000002 201 | 202 | // WindowFullScreen makes the window fills the entire screen without any frame around it. 203 | WindowFullScreen = 0x00000004 204 | ) 205 | -------------------------------------------------------------------------------- /viewer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "viewer.h" 9 | 10 | class QamelView : public QQuickView { 11 | Q_OBJECT 12 | 13 | public: 14 | QamelView(QWindow *parent = 0) : QQuickView(parent) {} 15 | QamelView(const QUrl &source, QWindow *parent = nullptr) : QQuickView(source, parent) {} 16 | 17 | public slots: 18 | void reload() { 19 | engine()->clearComponentCache(); 20 | setSource(source()); 21 | } 22 | }; 23 | 24 | void* Viewer_NewViewer() { 25 | return new QamelView(); 26 | } 27 | 28 | void Viewer_SetSource(void* ptr, char* url) { 29 | QamelView *view = static_cast(ptr); 30 | QMetaObject::invokeMethod(view, "setSource", Q_ARG(QUrl, QUrl(QString(url)))); 31 | } 32 | 33 | void Viewer_SetResizeMode(void* ptr, int resizeMode) { 34 | QamelView *view = static_cast(ptr); 35 | view->setResizeMode(QQuickView::ResizeMode(resizeMode)); 36 | } 37 | 38 | void Viewer_SetFlags(void* ptr, int flags) { 39 | QamelView *view = static_cast(ptr); 40 | view->setFlags(Qt::WindowFlags(flags)); 41 | } 42 | 43 | void Viewer_SetHeight(void* ptr, int height) { 44 | QamelView *view = static_cast(ptr); 45 | view->setHeight(height); 46 | } 47 | 48 | void Viewer_SetWidth(void* ptr, int width) { 49 | QamelView *view = static_cast(ptr); 50 | view->setWidth(width); 51 | } 52 | 53 | void Viewer_SetMaximumHeight(void* ptr, int height) { 54 | QamelView *view = static_cast(ptr); 55 | view->setMaximumHeight(height); 56 | } 57 | 58 | void Viewer_SetMaximumWidth(void* ptr, int width) { 59 | QamelView *view = static_cast(ptr); 60 | view->setMaximumWidth(width); 61 | } 62 | 63 | void Viewer_SetMinimumHeight(void* ptr, int height) { 64 | QamelView *view = static_cast(ptr); 65 | view->setMinimumHeight(height); 66 | } 67 | 68 | void Viewer_SetMinimumWidth(void* ptr, int width) { 69 | QamelView *view = static_cast(ptr); 70 | view->setMinimumWidth(width); 71 | } 72 | 73 | void Viewer_SetOpacity(void* ptr, double opacity) { 74 | QamelView *view = static_cast(ptr); 75 | view->setOpacity(opacity); 76 | } 77 | 78 | void Viewer_SetTitle(void* ptr, char* title) { 79 | QamelView *view = static_cast(ptr); 80 | view->setTitle(QString(title)); 81 | } 82 | 83 | void Viewer_SetVisible(void* ptr, bool visible) { 84 | QamelView *view = static_cast(ptr); 85 | view->setVisible(visible); 86 | } 87 | 88 | void Viewer_SetPosition(void* ptr, int x, int y) { 89 | QamelView *view = static_cast(ptr); 90 | view->setPosition(x, y); 91 | } 92 | 93 | void Viewer_SetIcon(void* ptr, char* fileName) { 94 | QamelView *view = static_cast(ptr); 95 | QIcon icon = QIcon(QString(fileName)); 96 | view->setIcon(icon); 97 | } 98 | 99 | void Viewer_Show(void* ptr) { 100 | QamelView *view = static_cast(ptr); 101 | view->show(); 102 | } 103 | 104 | void Viewer_ShowMaximized(void* ptr) { 105 | QamelView *view = static_cast(ptr); 106 | view->showMaximized(); 107 | } 108 | 109 | void Viewer_ShowMinimized(void* ptr) { 110 | QamelView *view = static_cast(ptr); 111 | view->showMinimized(); 112 | } 113 | 114 | void Viewer_ShowFullScreen(void* ptr) { 115 | QamelView *view = static_cast(ptr); 116 | view->showFullScreen(); 117 | } 118 | 119 | void Viewer_ShowNormal(void* ptr) { 120 | QamelView *view = static_cast(ptr); 121 | view->showNormal(); 122 | } 123 | 124 | void Viewer_SetWindowStates(void* ptr, int state) { 125 | QamelView *view = static_cast(ptr); 126 | view->setWindowStates(Qt::WindowStates(state)); 127 | } 128 | 129 | void Viewer_ClearComponentCache(void* ptr) { 130 | QamelView *view = static_cast(ptr); 131 | view->engine()->clearComponentCache(); 132 | } 133 | 134 | void Viewer_Reload(void* ptr) { 135 | QMetaObject::invokeMethod(static_cast(ptr), "reload"); 136 | } 137 | 138 | #include "moc-viewer.h" -------------------------------------------------------------------------------- /viewer.go: -------------------------------------------------------------------------------- 1 | package qamel 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include 7 | // #include "viewer.h" 8 | import "C" 9 | import ( 10 | "fmt" 11 | "os" 12 | fp "path/filepath" 13 | "strings" 14 | "time" 15 | "unsafe" 16 | 17 | "github.com/fsnotify/fsnotify" 18 | "github.com/sirupsen/logrus" 19 | ) 20 | 21 | // Viewer is the QML viewer which wraps QQuickView 22 | type Viewer struct { 23 | ptr unsafe.Pointer 24 | } 25 | 26 | // NewViewer constructs a QQuickView. 27 | func NewViewer() Viewer { 28 | ptr := C.Viewer_NewViewer() 29 | return Viewer{ptr: ptr} 30 | } 31 | 32 | // NewViewerWithSource constructs a QQuickView with the given QML source. 33 | func NewViewerWithSource(source string) Viewer { 34 | view := NewViewer() 35 | view.SetSource(source) 36 | return view 37 | } 38 | 39 | // SetSource sets the source to the url, loads the QML component and instantiates it. 40 | // The source could be a Qt resource path (qrc://icon) or a file path (file://path/to/icon). 41 | // However, it must be a valid path. 42 | func (view Viewer) SetSource(url string) { 43 | if view.ptr == nil { 44 | return 45 | } 46 | 47 | cURL := C.CString(url) 48 | defer C.free(unsafe.Pointer(cURL)) 49 | C.Viewer_SetSource(view.ptr, cURL) 50 | } 51 | 52 | // SetResizeMode sets whether the view should resize the window contents. 53 | // If this property is set to SizeViewToRootObject (the default), the view resizes 54 | // to the size of the root item in the QML. If this property is set to 55 | // SizeRootObjectToView, the view will automatically resize the root item to the 56 | // size of the view. 57 | func (view Viewer) SetResizeMode(resizeMode ResizeMode) { 58 | if view.ptr == nil { 59 | return 60 | } 61 | 62 | C.Viewer_SetResizeMode(view.ptr, C.int(resizeMode)) 63 | } 64 | 65 | // SetFlags sets the flags of the window. The window flags control the window's appearance 66 | // in the windowing system, whether it's a dialog, popup, or a regular window, and whether it 67 | // should have a title bar, etc. The actual window flags might differ from the flags set with 68 | // setFlags() if the requested flags could not be fulfilled. 69 | func (view Viewer) SetFlags(flags WindowFlags) { 70 | if view.ptr == nil { 71 | return 72 | } 73 | 74 | C.Viewer_SetFlags(view.ptr, C.int(flags)) 75 | } 76 | 77 | // SetHeight sets the height of the window. 78 | func (view Viewer) SetHeight(height int) { 79 | if view.ptr == nil { 80 | return 81 | } 82 | 83 | C.Viewer_SetHeight(view.ptr, C.int(int32(height))) 84 | } 85 | 86 | // SetWidth sets the width of the window. 87 | func (view Viewer) SetWidth(width int) { 88 | if view.ptr == nil { 89 | return 90 | } 91 | 92 | C.Viewer_SetWidth(view.ptr, C.int(int32(width))) 93 | } 94 | 95 | // SetMaximumHeight sets the maximum height of the window. 96 | func (view Viewer) SetMaximumHeight(height int) { 97 | if view.ptr == nil { 98 | return 99 | } 100 | 101 | C.Viewer_SetMaximumHeight(view.ptr, C.int(int32(height))) 102 | } 103 | 104 | // SetMaximumWidth sets the maximum width of the window. 105 | func (view Viewer) SetMaximumWidth(width int) { 106 | if view.ptr == nil { 107 | return 108 | } 109 | 110 | C.Viewer_SetMaximumWidth(view.ptr, C.int(int32(width))) 111 | } 112 | 113 | // SetMinimumHeight sets the minimum height of the window. 114 | func (view Viewer) SetMinimumHeight(height int) { 115 | if view.ptr == nil { 116 | return 117 | } 118 | 119 | C.Viewer_SetMinimumHeight(view.ptr, C.int(int32(height))) 120 | } 121 | 122 | // SetMinimumWidth sets the minimum width of the window. 123 | func (view Viewer) SetMinimumWidth(width int) { 124 | if view.ptr == nil { 125 | return 126 | } 127 | 128 | C.Viewer_SetMinimumWidth(view.ptr, C.int(int32(width))) 129 | } 130 | 131 | // SetOpacity sets the opacity of the window in the windowing system. If the windowing system supports 132 | // window opacity, this can be used to fade the window in and out, or to make it semitransparent. 133 | // A value of 1.0 or above is treated as fully opaque, whereas a value of 0.0 or below is treated as 134 | // fully transparent. Values inbetween represent varying levels of translucency between the two extremes. 135 | // The default value is 1.0. 136 | func (view Viewer) SetOpacity(opacity float64) { 137 | if view.ptr == nil { 138 | return 139 | } 140 | 141 | C.Viewer_SetOpacity(view.ptr, C.double(opacity)) 142 | } 143 | 144 | // SetTitle sets the window's title in the windowing system. The window title might appear in the title 145 | // area of the window decorations, depending on the windowing system and the window flags. It might also 146 | // be used by the windowing system to identify the window in other contexts, such as in the task switcher. 147 | func (view Viewer) SetTitle(title string) { 148 | if view.ptr == nil { 149 | return 150 | } 151 | 152 | cTitle := C.CString(title) 153 | defer C.free(unsafe.Pointer(cTitle)) 154 | C.Viewer_SetTitle(view.ptr, cTitle) 155 | } 156 | 157 | // SetVisible sets whether the window is visible or not. This property controls the visibility of the 158 | // window in the windowing system. By default, the window is not visible, you must call setVisible(true), 159 | // or show() or similar to make it visible. 160 | func (view Viewer) SetVisible(visible bool) { 161 | if view.ptr == nil { 162 | return 163 | } 164 | 165 | C.Viewer_SetVisible(view.ptr, C.bool(visible)) 166 | } 167 | 168 | // SetPosition sets the position of the window on the desktop to x, y. 169 | func (view Viewer) SetPosition(x int, y int) { 170 | if view.ptr == nil { 171 | return 172 | } 173 | 174 | C.Viewer_SetPosition(view.ptr, C.int(int32(x)), C.int(int32(y))) 175 | } 176 | 177 | // SetIcon sets the window's icon in the windowing system. The window icon might be used by the windowing 178 | // system for example to decorate the window, and/or in the task switcher. 179 | // Note: On macOS, the window title bar icon is meant for windows representing documents, and will only 180 | // show up if a file path is also set. 181 | func (view Viewer) SetIcon(fileName string) { 182 | if view.ptr == nil { 183 | return 184 | } 185 | 186 | cFileName := C.CString(fileName) 187 | defer C.free(unsafe.Pointer(cFileName)) 188 | C.Viewer_SetIcon(view.ptr, cFileName) 189 | } 190 | 191 | // Show shows the window. This is equivalent to calling showFullScreen(), showMaximized(), or 192 | // showNormal(), depending on the platform's default behavior for the window type and flags. 193 | func (view Viewer) Show() { 194 | if view.ptr == nil { 195 | return 196 | } 197 | 198 | C.Viewer_Show(view.ptr) 199 | } 200 | 201 | // ShowMaximized shows the window as maximized. 202 | // Equivalent to calling setWindowStates(WindowMaximized) and then setVisible(true). 203 | func (view Viewer) ShowMaximized() { 204 | if view.ptr == nil { 205 | return 206 | } 207 | 208 | C.Viewer_ShowMaximized(view.ptr) 209 | } 210 | 211 | // ShowMinimized shows the window as minimized. 212 | // Equivalent to calling setWindowStates(WindowMinimized) and then setVisible(true). 213 | func (view Viewer) ShowMinimized() { 214 | if view.ptr == nil { 215 | return 216 | } 217 | 218 | C.Viewer_ShowMinimized(view.ptr) 219 | } 220 | 221 | // ShowFullScreen shows the window as fullscreen. 222 | // Equivalent to calling setWindowStates(WindowFullScreen) and then setVisible(true). 223 | func (view Viewer) ShowFullScreen() { 224 | if view.ptr == nil { 225 | return 226 | } 227 | 228 | C.Viewer_ShowFullScreen(view.ptr) 229 | } 230 | 231 | // ShowNormal shows the window as normal, i.e. neither maximized, minimized, nor fullscreen. 232 | // Equivalent to calling setWindowStates(WindowNoState) and then setVisible(true). 233 | func (view Viewer) ShowNormal() { 234 | if view.ptr == nil { 235 | return 236 | } 237 | 238 | C.Viewer_ShowNormal(view.ptr) 239 | } 240 | 241 | // SetWindowStates sets the screen-occupation state of the window. The window state represents whether 242 | // the window appears in the windowing system as maximized, minimized and/or fullscreen. The window can 243 | // be in a combination of several states. For example, if the window is both minimized and maximized, 244 | // the window will appear minimized, but clicking on the task bar entry will restore it to the 245 | // maximized state. 246 | func (view Viewer) SetWindowStates(state WindowStates) { 247 | if view.ptr == nil { 248 | return 249 | } 250 | 251 | C.Viewer_SetWindowStates(view.ptr, C.int(state)) 252 | } 253 | 254 | // ClearComponentCache clears the engine's internal component cache. This function causes the property 255 | // metadata of all components previously loaded by the engine to be destroyed. All previously loaded 256 | // components and the property bindings for all extant objects created from those components will cease 257 | // to function. This function returns the engine to a state where it does not contain any loaded 258 | // component data. This may be useful in order to reload a smaller subset of the previous component set, 259 | // or to load a new version of a previously loaded component. Once the component cache has been cleared, 260 | // components must be loaded before any new objects can be created. 261 | func (view Viewer) ClearComponentCache() { 262 | if view.ptr == nil { 263 | return 264 | } 265 | 266 | C.Viewer_ClearComponentCache(view.ptr) 267 | } 268 | 269 | // Reload reloads the active QML view. 270 | func (view Viewer) Reload() { 271 | if view.ptr == nil { 272 | return 273 | } 274 | 275 | C.Viewer_Reload(view.ptr) 276 | } 277 | 278 | // WatchResourceDir watches for change inside the specified resource dir. 279 | // When change happened, the view will be reloaded immediately. 280 | // The directory path must be absolute. 281 | // Only use this in development environment. 282 | func (view Viewer) WatchResourceDir(dirPath string) { 283 | // Make sure directory is exists 284 | dirInfo, err := os.Stat(dirPath) 285 | if os.IsNotExist(err) || !dirInfo.IsDir() { 286 | logrus.Fatalf("directory %s does not exist\n", dirPath) 287 | } 288 | 289 | // Make sure directory path is absolute 290 | if !fp.IsAbs(dirPath) { 291 | logrus.Fatalln("path to directory must be absolute") 292 | } 293 | 294 | // Create watcher 295 | watcher, err := fsnotify.NewWatcher() 296 | if err != nil { 297 | logrus.Fatalln("failed to create watcher:", err) 298 | } 299 | defer watcher.Close() 300 | 301 | // Add all subdir inside resource dir to watcher 302 | err = fp.Walk(dirPath, func(path string, info os.FileInfo, _ error) error { 303 | if info.IsDir() { 304 | return watcher.Add(path) 305 | } 306 | return nil 307 | }) 308 | 309 | if err != nil { 310 | logrus.Fatalln("failed to scan resource dir:", err) 311 | } 312 | 313 | // Watch for files change 314 | logrus.Infoln("File watcher enabled for", dirPath) 315 | logrus.Infoln("Only use it in safe environment") 316 | 317 | lastEvent := struct { 318 | Name string 319 | Time time.Time 320 | }{} 321 | 322 | for { 323 | select { 324 | case event := <-watcher.Events: 325 | // Make sure the file is not qmlc or jsc 326 | fileName := event.Name 327 | if fp.Ext(fileName) == ".qmlc" || strings.Contains(fileName, ".qmlc.") || 328 | fp.Ext(fileName) == ".jsc" || strings.Contains(fileName, ".jsc.") { 329 | continue 330 | } 331 | 332 | // In some OS, the write events fired twice. 333 | // To fix this, check if current event is happened less than one sec before. 334 | // If yes, skip this event. 335 | now := time.Now() 336 | eventName := fmt.Sprintf("%s: %s", event.Op.String(), fileName) 337 | if lastEvent.Name == eventName && now.Sub(lastEvent.Time).Seconds() <= 1.0 { 338 | continue 339 | } 340 | 341 | // Also make sure that file is not empty 342 | if info, err := os.Stat(fileName); err != nil || info.Size() == 0 { 343 | continue 344 | } 345 | 346 | // Else, save this event and reload view. 347 | lastEvent = struct { 348 | Name string 349 | Time time.Time 350 | }{Name: eventName, Time: now} 351 | 352 | logrus.Println(eventName) 353 | view.Reload() 354 | case err := <-watcher.Errors: 355 | if err != nil { 356 | logrus.Errorln("Watcher error:", err) 357 | } 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /viewer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef QAMEL_VIEWER_H 4 | #define QAMEL_VIEWER_H 5 | 6 | #include 7 | #include 8 | 9 | #ifdef __cplusplus 10 | 11 | // Class 12 | class QamelView; 13 | 14 | extern "C" { 15 | #endif 16 | 17 | // Constructor 18 | void* Viewer_NewViewer(); 19 | 20 | // Methods 21 | void Viewer_SetSource(void* ptr, char* url); 22 | void Viewer_SetResizeMode(void* ptr, int resizeMode); 23 | void Viewer_SetFlags(void* ptr, int flags); 24 | void Viewer_SetHeight(void* ptr, int height); 25 | void Viewer_SetWidth(void* ptr, int width); 26 | void Viewer_SetMaximumHeight(void* ptr, int height); 27 | void Viewer_SetMaximumWidth(void* ptr, int width); 28 | void Viewer_SetMinimumHeight(void* ptr, int height); 29 | void Viewer_SetMinimumWidth(void* ptr, int width); 30 | void Viewer_SetOpacity(void* ptr, double opacity); 31 | void Viewer_SetTitle(void* ptr, char* title); 32 | void Viewer_SetVisible(void* ptr, bool visible); 33 | void Viewer_SetPosition(void* ptr, int x, int y); 34 | void Viewer_SetIcon(void* ptr, char* fileName); 35 | void Viewer_Show(void* ptr); 36 | void Viewer_ShowMaximized(void* ptr); 37 | void Viewer_ShowMinimized(void* ptr); 38 | void Viewer_ShowFullScreen(void* ptr); 39 | void Viewer_ShowNormal(void* ptr); 40 | void Viewer_SetWindowStates(void* ptr, int state); 41 | void Viewer_ClearComponentCache(void* ptr); 42 | void Viewer_Reload(void* ptr); 43 | 44 | #ifdef __cplusplus 45 | } 46 | #endif 47 | 48 | #endif --------------------------------------------------------------------------------