EasyRPG / Editor

Game editor similar to RPG Maker
https://easyrpg.org/editor/
GNU General Public License v3.0
336 stars 59 forks source link

Add QML Bindings for (almost all) liblcf data #205

Open Ghabry opened 2 years ago

Ghabry commented 2 years ago

This expoeses liblcf data structures to QML. QWidgets are kinda obsolute, so this is a first step in direction of QML.

Without this binding using QML is just extremely painful because the entire API relies on the Meta Object Compiler.

It uses a code generator. I investigated manual alternatives for many days and a generator just works best here.

The binding is holding pointers to all the lcf data, so it is a thin, non-owning wrapper. Of course all this wrapping is not for free but that cannot be really avoided when crossing C++ <-> Javascript/QML boundaries. But this is an editor so performance is not a mayor concern compared to the Player.

With this change it is possible to write code like this in QML:

var actor = project.database.actors.get(1)
console.log(actor.name)

Also data binding becomes super easy this way. E.g. to edit the current actor name in the QML Ui you can write (just an example, not included yet):

Controls.TextField {
    Kirigami.FormData.label: "Name:"
    text: obj.name // obj is a wrapper around lcf::rpg::Actor. All fields are available!

    onTextChanged: {
        obj.name = text
    }
}

We could even provide a way to generate e.g. these Actor curves from a expression. Want quadratic hp growth? Easy:

var hp = actor.parameter.maxhp;
for (var i = 0; i < hp.length; ++i) {
  hp = pow(i + 1, 2);
}

Content:

To demonstrate that the extension API works I added as an example to Actor:

Q_INVOKABLE void sayHello() {
    std::cout << "Hello from " << m_data.name.c_str() << "\n";
}

Now all actors have the new method sayHello available in QML.


TODO I will resolve when it starts to become a problem:

jetrotal commented 2 years ago

Hi, I have made a library for visual Assets called "ez" with my limited knowledge on QML. Maybe you can cut off the functions and var appButtons, since they list the menus and items, and you made a better way of Binding elements that are already on the liblcf.

property string assets: pathIsLocal ? Qt.resolvedUrl(".") : "https://raw.githubusercontent.com/jetrotal/easyRPG_UI/main/";

background:ez.bg;

component UI_Library : Item {
        visible:false
        property real resizeFactor: 1
        property var style: {

            "sizes": {
                "text":9 * resizeFactor,
                "topBar":27 * resizeFactor,
                "logo": 16 * resizeFactor,
                "win":16 * resizeFactor,
                "icon":24 * resizeFactor,
                "checkboxSpacing":15 * resizeFactor,
                "checkboxOuter":11 * resizeFactor,
                "checkboxInner":5 * resizeFactor,

            },

            "colors": {
                "bg": "#292b2f",
                "header": "#242529",
                "textA": "#97999C",
                "textB": "#FFF",
                "highlight": "#8AFF4E",
                "separator": "#555",
                "darkBorders": "#1e1e1e",
                "shadowColor":"#222327"
            },

            "images": {
                "logo": assets + "img/ez-logo.svg",
                "bg": assets + "img/bg/tile.svg",
                "icons": {
                    "win": {
                        "url": assets + "img/icons/window/"
                    },
                    "top": {
                        "url": assets + "img/icons/top/"
                    }
                }
            }
        } // STYLES
        property var appButtons: {
            "workSpace": [{
                "obj": "appHome",
                "display": "Home",
            },/*separator*/, {
                "obj": "mapEditor",
                "display": "Map Editor",
            }, {
                "obj": "databaseEditor",
                "display": "Database Editor",
            }, {
                "obj": "eventsEditor",
                "display": "Events Editor",
            },/*separator*/, {
                "obj": "resourceManager",
                "display": "Resource Manager",
            }, {
                "obj": "gameSearch",
                "display": "Search",
            },/*separator*/, {
                "obj": "bgmTest",
                "display": "BGM test",
            },/*separator*/, {
                "obj": "gameSettings",
                "display": "Playtest Settings",
            }, {
                "obj": "gamePlay",
                "display": "Begin Playtest",
            }],
            "window": [{
                "obj": "min",
                "display": "Minimize Window",
            }, {
                "obj": "max",
                "display": "Maximize Window",
            }, {
                "obj": "close",
                "display": "Close Window",
            }],
            "topBar": [{
                "obj": "project",
                "display": "Game Project",
                "contents": [{
                    "obj":"newProject","display":"New Project", "shortcut":"Ctrl+N", "disabled":1
                }, {
                    "obj":"openProject","display":"Open Project", "shortcut":"Ctrl+O", "disabled":1
                }, {
                    "obj":"closeProject","display":"Close Project",  "shortcut":"Ctrl+X"
                },/*separator*/, {
                    "obj":"createDisk","display":"Create Game Disk"
                },/*separator*/, {
                    "obj":"quit","display":"Quit"
                }]
            }, {
                "obj": "maps",
                "display": "Maps",
                "contents": [{
                    "obj":"saveMap","display":"Save Map", "shortcut":"Ctrl+S"
                }, {
                    "obj":"revertMap","display":"Revert Map", "shortcut":"Ctrl+R"
                },/*separator*/, {
                    "obj":"lowerLayer","display":"Lower Layer", "shortcut":"F5"
                }, {
                    "obj":"upperLayer","display":"Upper Layer", "shortcut":"F6"
                }, {
                    "obj":"eventsLayer","display":"Events Layer", "shortcut":"F7"
                },/*separator*/, {
                    "obj":"defaultZoom","display":"Zoom 100%"
                }, {
                    "obj":"zoomIn","display":"Zoom In", "shortcut":"+"
                }, {
                    "obj":"zoomOut","display":"Zoom Out", "shortcut":"-"
                }]
            }, {
                "obj": "view",
                "display": "View",
                "contents": [{
                    "obj":"palette","display":"Palette","checkbox":1,"checked":1
                }, {
                    "obj":"mapTree","display":"Map Tree","checkbox":1,"checked":1
                }]
            }, {
                "obj": "tools",
                "display": "Tools",
                "contents": [{
                    "obj":"database","display":"DataBase", "shortcut":"F8"
                }, {
                    "obj":"resourceManager","display":"Resource Manager"
                }, {
                    "obj":"jukebox","display":"Jukebox"
                }, {
                    "obj":"search","display":"Search"
                },]
            }, {
                "obj": "game",
                "display": "Game",
                "contents": [{
                    "obj":"playTest","display":"PlayTest", "shortcut":"F4"
                },/*separator*/, {
                    "obj":"fullScreen","display":"Fullscreen", "checkbox":1,"checked":1
                }, {
                    "obj":"showTitle","display":"Show Title Background","checkbox":1,"checked":1
                },/*separator*/, {
                    "obj":"playSettings","display":"PlayTest Settings"
                },]
            }, {
                "obj": "help",
                "display": "Help",
                "contents": [{
                    "obj":"contents","display":"Contents", "shortcut":"F1"
                },/*separator*/, {
                    "obj":"about","display":"About"
                }, {
                    "obj":"aboutQt","display":"About Qt"
                },]
            }, {
                "obj": "debug",
                "display": "Debug",
                "contents": [{
                    "obj":"rtpPath","display":"RTP Path"
                }, {
                    "obj":"caching","display":"Enable Caching","checkbox":1,"checked":1
                },/*separator*/, {
                    "obj":"displayButtons","display":"Display Button Actions","checkbox":1,"checked":debugParams.displayButtons * debug
                }, {
                                   "obj":"displayBorders","display":"Display Invisible Borders","checkbox":1,"checked":debugParams.displayBorders * debug
                               },, {
                                   "obj":"listProperties","display":"List Properties on Console","checkbox":1,"checked":debugParams.listProperties * debug
                               },]
            }]
        }

        function listProperties(item) {
            // debug props
            var result="";
            for (var p in item) result += String(item[p]) !== "function() { [native code] }" ? (p +": " + (String(item[p]) ==="[object Object]" ?" {; "+ listProperties(item[p])+" } " : item[p]) +`;`):"";
            return(result.split(';').join(pathIsLocal ? "\n" : "<br>"))
        }

        function detectCommand(bt, type,qmlItem) {
            if (type[1] === "debug") debugFunction(bt)
            if (debugParams.displayButtons===1) footerMark.text = ("running detectCommand() - You clicked: " + JSON.stringify(bt.obj) + "  |  Type: "+JSON.stringify(type))
            else footerMark.text="";

            if (debugParams.listProperties===1) console.log(ez.listProperties(qmlItem))

            if (type === "workspace") return changeWorkspace(bt)
            if (type === "window") return resizeWindow(bt)

        }

        function debugFunction(bt){
            console.log(bt.obj +": "+debugParams[bt.obj])
            debugParams[bt.obj] = debugParams[bt.obj] === 0 ? 1 : 0
            contentBody.border.width = debugParams.displayBorders
        }

        function resizeWindow(bt) {
            if (bt.obj === "min") return app.showMinimized()
            if (bt.obj === "max") return console.log(app.visibility), app.visibility === 4 ? app.showNormal() :app.showMaximized()
            if (bt.obj === "close") return app.close()
        }

        function changeWorkspace(e) {
            currWorkspace.text = "<pre><font color='" + styles.colors.textB + "'>  " + e.display + "  </pre>"
        }

        property Item bg: Rectangle {
            anchors.fill: parent
            color: ez.style.colors.bg
            Image {
                anchors.fill:parent
                fillMode: Image.Tile
                source: ez.style.images.bg
            }
        }

    }
    UI_Library {
        id:ez
    } // ez[variableName] // ez.styles[styleVar] //

The full file is here: https://github.com/jetrotal/easyRPG_UI/blob/main/scripts/qml/index.qml and the images folder is here: https://github.com/jetrotal/easyRPG_UI/tree/main/img

I failed to make it work as a typical QML file since I was not able to install the Editor's projec, or kirigami or any other library on my computer. But, I hope that helps on something;