├── .gitignore ├── LICENSE ├── README.md ├── dub.sdl ├── example ├── app.d ├── dub.sdl └── ui │ ├── main.glade │ └── panel.glade └── source └── gtkui ├── base.d ├── builder.d ├── exception.d └── package.d /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | *.a 3 | example/example 4 | dub.selections.json 5 | *.*~ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Oleg Butko (deviator) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Gtk UI aux library for using with UI Builder `Glade` 2 | 3 | ```d 4 | import gtk.Button; 5 | import gtk.Window; 6 | import gtkui; 7 | 8 | void main() 9 | { 10 | auto ui = new UI; 11 | ui.addOnQuit({ ui.exitLoop(); }); 12 | ui.runLoop(); 13 | } 14 | 15 | class UI : MainBuilderUI 16 | { 17 | mixin GtkBuilderHelper; 18 | 19 | @gtkwidget Window mwindow; 20 | @gtkwidget Button addbtn; 21 | 22 | @gtksignal void someAction() 23 | { 24 | /+ do something +/ 25 | } 26 | 27 | this() 28 | { 29 | super(import("main.glade")); 30 | addbtn.addOnClicked((b) { /+ do something +/ }); 31 | setupMainWindow(mwindow); 32 | } 33 | } 34 | ``` 35 | 36 | For more information see small (~130 lines with comments) [example](example/app.d) 37 | 38 | #### Limitations in signal usage 39 | 40 | 1. Signals in glade file should not contain `User data`, 41 | otherwise this will crash program. 42 | 43 | 2. If you want to use parameters from signal method you must be sure of 44 | their count and order, otherwise wrong count or order of parameters 45 | will crash program. 46 | 47 | 3. Parameter of signal method must be gtk pointers (not wraps from gtkD). 48 | 49 | 4. If you want don't use parameters for signal method be sure what this signal 50 | receive only two parameters (gtk widget and user data), 51 | otherwise you should specify parameters. -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "gtkui" 2 | targetType "library" 3 | description "Aux library for writing GTK-interface program" 4 | authors "Oleg Butko (deviator)" 5 | copyright "Copyright © 2017-2018, Oleg Butko" 6 | license "MIT" 7 | dependency "gtk-d:gtkd" version="~>3.9.0" -------------------------------------------------------------------------------- /example/app.d: -------------------------------------------------------------------------------- 1 | // basic use of gtkui 2 | import std.conv : to; 3 | import std.stdio : stderr; 4 | 5 | // import all needed gtk classes 6 | import gtk.Box; // layout container 7 | import gtk.Button; 8 | import gtk.Label; 9 | import gtk.Widget; // base class for all widgets 10 | import gtk.Window; 11 | 12 | import gtkui; 13 | 14 | void main() 15 | { 16 | auto ui = new UI; 17 | // on close main window exit from gtk.Main loop 18 | ui.addOnQuit({ ui.exitLoop(); }); 19 | // run gtk.Main loop 20 | ui.runLoop(); 21 | } 22 | 23 | // main window (main ui class) 24 | class UI : MainBuilderUI 25 | { 26 | /+ override method that find `@gtkwidget` fields 27 | and get instances of those fields from builder 28 | override method that find `@gtksignal` methods 29 | and connect those methods in builder 30 | +/ 31 | mixin GtkBuilderHelper; 32 | 33 | // widgets definition 34 | @gtkwidget Window mwindow; 35 | @gtkwidget Box vbox; 36 | @gtkwidget Button addbtn; 37 | 38 | @gtksignal void clickAdd(GtkButton* btnptr) 39 | { 40 | new Button(btnptr).setLabel("add " ~ (panels_count+1).to!string); 41 | } 42 | 43 | @gtksignal void setFocus(GtkContainer* c, GtkWidget* e) 44 | { 45 | stderr.writeln(" set focus callback: ", 46 | cast(void*)vbox.getBoxStruct(), " = ", 47 | cast(void*)c, " widget: ", cast(void*)e); 48 | } 49 | 50 | @gtksignal(true) void setFocusSwapped(GtkWidget* e, GtkContainer* c) 51 | { 52 | stderr.writeln("set focus swapped callback: ", 53 | cast(void*)vbox.getBoxStruct(), " = ", 54 | cast(void*)c, " widget: ", cast(void*)e); 55 | } 56 | 57 | uint panels_count; 58 | 59 | this() 60 | { 61 | enum CUSTOMCSS = ""; // you can customize ui 62 | 63 | // get "main.glade" from string import paths at compile-time 64 | // you can use run-time loading glade files 65 | super(import("main.glade"), CUSTOMCSS); 66 | 67 | // widgets automaticaly gets from builder and you 68 | // can use those without any boilereplate code 69 | addbtn.addOnClicked((b) 70 | { 71 | // create new dynamic part of ui and add this to `vbox` 72 | auto tmp = new Panel(panels_count++); 73 | vbox.packEnd(tmp.mainWidget, true, true, 12); 74 | vbox.setFocusChild(tmp.mainWidget); 75 | }); 76 | 77 | // add calling `quit()` on hide mwindow 78 | // and call `showAll()` for mwindow 79 | setupMainWindow(mwindow); 80 | } 81 | } 82 | 83 | // part of ui -- `ChildBuilder` have no method for working with 84 | // gtk.Main and used as part of main ui, but have own builder 85 | // and requires a glade file in ctor 86 | class Panel : ChildBuilderUI 87 | { 88 | mixin GtkBuilderHelper; 89 | 90 | uint idx; 91 | 92 | // child parts 93 | Foo!"g1" g1; 94 | Foo!"g2" g2; 95 | 96 | @gtkwidget Box panelmainbox; 97 | 98 | // as for widgets signals can have namespace 99 | // at glade file signal should have name "panel.clickG2" 100 | @gtksignal("panel") void clickG2() 101 | { 102 | stderr.writefln("clickG2 signal (from %d)", idx); 103 | } 104 | 105 | this(uint idx) 106 | { 107 | this.idx = idx; 108 | super(import("panel.glade")); 109 | g1 = new typeof(g1)(this); 110 | g2 = new typeof(g2)(this); 111 | } 112 | 113 | override Widget mainWidget() @property { return panelmainbox; } 114 | } 115 | 116 | // `ChildGtkUI` most simplest implementation of GtkUI and can't be used 117 | // independently of some `GtkUI` that provides `getObject` method as parent 118 | class Foo(string NAME) : ChildGtkUI 119 | { 120 | mixin GtkUIHelper; 121 | 122 | // you can use prefix in glade file for widget names ("g1", and "g2") 123 | @gtkwidget(NAME) 124 | { 125 | Button btn; // "g1.btn" or "g2.btn" 126 | Label label; // "g1.label" or "g2.label" 127 | } 128 | 129 | this(GtkUI parent) 130 | { 131 | super(parent); 132 | btn.addOnClicked((b) // some behavior 133 | { label.setText((label.getText.to!uint + 1).to!string); }); 134 | } 135 | } -------------------------------------------------------------------------------- /example/dub.sdl: -------------------------------------------------------------------------------- 1 | name "example" 2 | targetType "executable" 3 | stringImportPaths "ui" 4 | dependency "gtkui" path="../" 5 | sourceFiles "app.d" -------------------------------------------------------------------------------- /example/ui/main.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | 8 | 9 | True 10 | False 11 | 12 12 | 12 13 | 12 14 | 12 15 | vertical 16 | 12 17 | 18 | 19 | True 20 | False 21 | 12 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | True 30 | True 31 | 0 32 | 33 | 34 | 35 | 36 | add 37 | True 38 | True 39 | True 40 | 41 | 42 | 43 | False 44 | True 45 | 1 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/ui/panel.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | gtk-go-up 9 | 10 | 11 | True 12 | False 13 | gtk-go-up 14 | 15 | 16 | True 17 | False 18 | 6 19 | 6 20 | 6 21 | 6 22 | vertical 23 | 12 24 | 25 | 26 | True 27 | False 28 | 0 29 | in 30 | 31 | 32 | True 33 | False 34 | 6 35 | 6 36 | 6 37 | 6 38 | 12 39 | 40 | 41 | True 42 | False 43 | 1 44 | 45 | 46 | True 47 | True 48 | 0 49 | 50 | 51 | 52 | 53 | True 54 | True 55 | True 56 | image1 57 | True 58 | 59 | 60 | False 61 | True 62 | 1 63 | 64 | 65 | 66 | 67 | 68 | 69 | True 70 | False 71 | g1 72 | 73 | 74 | 75 | 76 | False 77 | True 78 | 0 79 | 80 | 81 | 82 | 83 | True 84 | False 85 | 0 86 | in 87 | 88 | 89 | True 90 | False 91 | 6 92 | 6 93 | 6 94 | 6 95 | vertical 96 | 12 97 | 98 | 99 | True 100 | True 101 | True 102 | image2 103 | True 104 | 105 | 106 | 107 | False 108 | True 109 | 0 110 | 111 | 112 | 113 | 114 | True 115 | False 116 | 1 117 | 118 | 119 | False 120 | True 121 | 1 122 | 123 | 124 | 125 | 126 | 127 | 128 | True 129 | False 130 | g2 131 | 132 | 133 | 134 | 135 | False 136 | True 137 | 1 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /source/gtkui/base.d: -------------------------------------------------------------------------------- 1 | /// Base types for creating GUI controllers 2 | module gtkui.base; 3 | 4 | import gtkui.exception; 5 | 6 | import gobject.ObjectG; 7 | 8 | /// Base GUI interface type 9 | interface GtkUI 10 | { 11 | /// For `@gtkwidget` UDA 12 | protected struct WidgetUDA { string ns; } 13 | /// ditto 14 | static protected auto gtkwidget(string ns="") @property 15 | { return WidgetUDA(ns); } 16 | 17 | /// Automatic initialize `@gtkwidget` fields 18 | protected void setUpGtkWidgetFields(); 19 | /// 20 | protected ObjectG getObject(string name); 21 | 22 | /++ Returns: 23 | casted ui object by name (widget for example) 24 | +/ 25 | auto ui(T)(string name, string file=__FILE__, size_t line=__LINE__) 26 | if (is(T : ObjectG)) 27 | { 28 | import std.format : format; 29 | import std.exception : enforce; 30 | auto obj = enforce(getObject(name), 31 | new GUIException(format("can't get object '%s'", name), file, line)); 32 | 33 | return enforce(cast(T)obj, 34 | new GUIException(format("object '%s' has type '%s', not '%s'", 35 | name, obj.getType(), typeid(T).name), file, line)); 36 | } 37 | 38 | /++ Insert this mixin in all child classes where used `@gtkwidget` 39 | contains: 40 | implementation of `void setUpGtkWidgetFields()` 41 | +/ 42 | mixin template GtkUIHelper() 43 | { 44 | protected override void setUpGtkWidgetFields() 45 | { 46 | import std.traits : hasUDA, getUDAs; 47 | import std.format : format; 48 | import gobject.ObjectG; 49 | 50 | alias T = typeof(this); 51 | 52 | foreach (m; __traits(allMembers, typeof(this))) 53 | { 54 | static if (hasUDA!(__traits(getMember, T, m), WidgetUDA)) 55 | { 56 | enum uda = getUDAs!(__traits(getMember, T, m), WidgetUDA)[0]; 57 | enum name = (uda.ns.length ? uda.ns ~ "." : "") ~ m; 58 | alias F = typeof(__traits(getMember, T, m)); 59 | static assert(is(F : ObjectG), format("%s is not an ObjectG", F.stringof)); 60 | __traits(getMember, T, m) = ui!F(name); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | /// Need parent `GtkUI` for working 68 | class ChildGtkUI : GtkUI 69 | { 70 | mixin GtkUIHelper; 71 | 72 | /// 73 | protected GtkUI parent; 74 | 75 | /// 76 | this(GtkUI parent) 77 | { 78 | this.parent = parent; 79 | setUpGtkWidgetFields(); 80 | } 81 | 82 | /// 83 | override ObjectG getObject(string name) 84 | { return parent.getObject(name); } 85 | } -------------------------------------------------------------------------------- /source/gtkui/builder.d: -------------------------------------------------------------------------------- 1 | /// Use gtk.Builder for loading interface 2 | module gtkui.builder; 3 | 4 | import std.experimental.logger; 5 | import std.exception : enforce; 6 | 7 | import gtk.Builder; 8 | import gtk.Widget; 9 | import gtk.Window; 10 | import gobject.ObjectG; 11 | 12 | import gtkui.base; 13 | import gtkui.exception; 14 | 15 | /// 16 | abstract class BuilderUI : GtkUI 17 | { 18 | /// For `@gtksignal` UDA 19 | protected struct SignalUDA { string ns; bool swapped; } 20 | /// ditto 21 | static protected auto gtksignal(string ns="", bool swapped=false) @property 22 | { return SignalUDA(ns, swapped); } 23 | /// ditto 24 | static protected auto gtksignal(bool swapped) @property 25 | { return SignalUDA("", swapped); } 26 | 27 | mixin GtkUIHelper; 28 | 29 | /// parse xml ui and create elements 30 | protected Builder builder; 31 | 32 | /++ Insert this mixin in all builder classes where need use signals 33 | contains: 34 | implementation of `void setUpGtkSignals()` 35 | static extern(C) callback's for signals 36 | +/ 37 | mixin template GtkBuilderHelper() 38 | { 39 | mixin GtkUIHelper; 40 | 41 | import std.traits : hasUDA; 42 | 43 | alias This = typeof(this); 44 | 45 | private static string[] signatureNames(string m)() 46 | { 47 | import std.format : format; 48 | alias F = __traits(getMember, This, m); 49 | string[] ret; 50 | foreach (i, p; Parameters!F) 51 | ret ~= format!"p%d"(i); 52 | return ret; 53 | } 54 | 55 | import std.string : join; 56 | import std.traits : Parameters; 57 | 58 | private static string getSignature(string m)() 59 | { 60 | import std.traits : getUDAs; 61 | import std.algorithm : map; 62 | import std.range : enumerate; 63 | import std.format : format; 64 | enum swapped = getUDAs!(__traits(getMember, This, m), SignalUDA)[0].swapped; 65 | enum sNames = signatureNames!m; 66 | enum params = sNames.length ? 67 | sNames.enumerate.map!(a=>"Parameters!("~m~")["~a.index.to!string~"] "~a.value).join(", ") 68 | : `void* obj`; 69 | return swapped ? `void* user_data, `~params : params~`, void* user_data`; 70 | } 71 | 72 | static foreach (m; __traits(allMembers, This)) 73 | { 74 | static if (hasUDA!(__traits(getMember, This, m), SignalUDA)) 75 | { 76 | mixin(`protected static extern(C) void __g_signal_callback_`~m~`(`~getSignature!m~`) 77 | { 78 | import gtkui.exception; 79 | import std.exception : enforce; 80 | 81 | auto t = enforce(cast(This)(user_data), 82 | new GUIException("user data pointer for signal '`~m~`' is not '`~This.stringof~`'")); 83 | t.`~m~`(`~signatureNames!(m).join(", ")~`); 84 | }`); 85 | } 86 | } 87 | 88 | protected override void setUpGtkSignals() 89 | { 90 | import std.traits : getUDAs, isCallable; 91 | 92 | static foreach (m; __traits(allMembers, This)) 93 | { 94 | static if (hasUDA!(__traits(getMember, This, m), SignalUDA)) 95 | {{ 96 | static if (!isCallable!(__traits(getMember, This, m))) 97 | static assert(0, "signal can be only callable object (field '"~m~"')"); 98 | enum uda = getUDAs!(__traits(getMember, This, m), SignalUDA)[0]; 99 | enum name = (uda.ns.length ? uda.ns ~ "." : "") ~ m; 100 | mixin(`builder.addCallbackSymbol(name, cast(GCallback)(&__g_signal_callback_`~m~`));`); 101 | }} 102 | } 103 | builder.connectSignals(cast(void*)this); 104 | } 105 | } 106 | 107 | /// 108 | this(string xml) 109 | { 110 | builder = new Builder; 111 | enforce(builder.addFromString(xml), new GUIException("cannot create ui")); 112 | setUpGtkWidgetFields(); 113 | setUpGtkSignals(); 114 | } 115 | 116 | /// 117 | protected void setUpGtkSignals() {} 118 | 119 | /// Use builder for getting objects 120 | override ObjectG getObject(string name) { return builder.getObject(name); } 121 | } 122 | 123 | /// Class for main UI controller 124 | class MainBuilderUI : BuilderUI 125 | { 126 | import glib.Idle; 127 | static import gtk.Main; 128 | alias GtkMain = gtk.Main.Main; 129 | 130 | protected: 131 | static bool __gtk_inited = false; 132 | 133 | /// run Main.init with empty args by default 134 | static void initializeGtk(string[] args=[]) 135 | { 136 | if (__gtk_inited) return; 137 | __gtk_inited = true; 138 | GtkMain.init(args); 139 | } 140 | 141 | /// 142 | void delegate()[] onQuitList; 143 | 144 | /// 145 | void quit() { foreach(dlg; onQuitList) dlg(); } 146 | 147 | public: 148 | 149 | /// 150 | Idle[] idles; 151 | 152 | /// 153 | void addOnIdle(void delegate() fnc) 154 | { idles ~= new Idle({ fnc(); return true; }); } 155 | 156 | /// 157 | void runLoop() { GtkMain.run(); } 158 | 159 | /// 160 | bool loopStep(bool block=false) 161 | { return GtkMain.iterationDo(block); } 162 | 163 | /// 164 | void exitLoop() { GtkMain.quit(); } 165 | 166 | /// 167 | void addOnQuit(void delegate() dlg) { onQuitList ~= dlg; } 168 | 169 | /// 170 | protected static string lastUsedCss; 171 | 172 | /// 173 | static void updateStyle(string css, bool throwOnError=false) 174 | { 175 | import gtk.CssProvider; 176 | import gdk.Screen; 177 | import gtk.StyleContext; 178 | import std.typecons : scoped; 179 | 180 | if (css != lastUsedCss) 181 | { 182 | lastUsedCss = css; 183 | try 184 | { 185 | auto prov = scoped!CssProvider; 186 | prov.loadFromData(css); 187 | 188 | StyleContext.addProviderForScreen(Screen.getDefault(), 189 | prov, GTK_STYLE_PROVIDER_PRIORITY_USER); 190 | } 191 | catch (Throwable e) 192 | { 193 | .error("error while loading style: ", e.msg); 194 | if (throwOnError) throw e; 195 | } 196 | } 197 | } 198 | 199 | /// 200 | this(string xml, string css="") 201 | { 202 | initializeGtk(); 203 | super(xml); 204 | if (css.length) 205 | updateStyle(css); 206 | } 207 | 208 | /++ 209 | add on hide calling quit method 210 | +/ 211 | void setupMainWindow(Window w) 212 | { 213 | w.addOnHide((Widget){ quit(); }); 214 | w.showAll(); 215 | } 216 | } 217 | 218 | /// For child UI controllers 219 | class ChildBuilderUI : BuilderUI 220 | { 221 | /// 222 | this(string xml) { super(xml); } 223 | 224 | /// For adding by parent controller 225 | abstract Widget mainWidget() @property; 226 | } -------------------------------------------------------------------------------- /source/gtkui/exception.d: -------------------------------------------------------------------------------- 1 | /// 2 | module gtkui.exception; 3 | 4 | /// 5 | class GUIException : Exception 6 | { 7 | this(string msg, string file=__FILE__, size_t line=__LINE__) 8 | pure nothrow @nogc @safe 9 | { super(msg, file, line); } 10 | } -------------------------------------------------------------------------------- /source/gtkui/package.d: -------------------------------------------------------------------------------- 1 | /// 2 | module gtkui; 3 | 4 | public import gtkui.base; 5 | public import gtkui.builder; 6 | public import gtkui.exception; --------------------------------------------------------------------------------