Archive
Smart Mobile Studio 3.0 and beyond
With Smart Mobile Studio 3.0 entering its second beta, Smart Pascal developers are set for a boost in quality, creativity and power. We have worked extremely hard on the product this past year, including a complete rewrite of all our visual controls (and I mean all). We also introduced a completely new theme engine, one that completely de-couples visual appearance from structural architecture (it also allows scripting inside the CSS theme files).
All of that could be enough for a version bump, but we didn’t stop there. Much of the sub-strata in Smart has been re-implemented. Focus has been on stability, speed and future growth. The system is now divided into a set of name-spaces (System, SmartCL, SmartNJ, Phonegap, and Espruino), making it easier to navigate between the units as well as expanding the codebase in the future.
To better understand the namespaces and why this is a good idea, let’s go through how our units are organized.
- The System namespace is the foundation. It contains clean, platform independent code. Meaning code that doesn’t rely on the DOM (browser) or runtime (node). Focus here is on universal code, and to establish common object-pascal classes.
- Our SmartCL namespace contains visual code, meaning code and controls that targets the browser and the DOM. SmartCL rests on the System namespace and draws functionality from it. Through partial classes we also expand classes introduced in the system namespace. A good example is System.Time.pas and SmartCL.Time.pas. The latter expands the class TW3Dispatch with functionality that will only work in the DOM.
- SmartNJ is our high-level nodejs namespace. Here you find classes with fairly complex behavior such as servers, memory buffers, processes and auxillary classes. SmartNJ draws from the system namespace just like SmartCL. This was done to avoid multiple implementations of streams, utility classes and common functions. Being able to enjoy the same functionality under all platforms is a very powerful thing.
- Phonegap is our namespace for mobile devices. A mobile application is basically a normal visual application using SmartCL, but where you access extra functionality through phonegap. Things like access to a device’s photos, filesystem, dialogs and so on is all delegated via phonegap.
- Espruino is a namespace for working with Espruino micro-controllers. This has been a very low-level affair so far, due to size limitation on these devices. But with our recent changes you can now, when you need to, tap into the system namespace for more demanding behavior.
As you can see there is a lot of cool stuff in Smart Mobile Studio, and our codebase is maturing nicely. With out new organization we are able to expand both horizontally and vertically without turning the codebase into a gigantic mess (the VCL being a prime example of how not to implement a multi-platform framework).
Common behavior
One of the coolest things we have added has to be the new storage device classes. As you probably know the browser has a somewhat “limited” storage mechanism. You are stuck with name-value pairs in the cache, or a filesystem that is profoundly frustrating to work with. To remedy this we took the time to implement a virtual filesystem (in memory filesystem) that emits data to the cache; we also implemented a virtual storage device stack on top of it, one for each target (!).
In short, if a target has IO capability, we have implemented a storage “driver” for it. So instead of you having to write 4-5 different storage mechanisms – you can now write the storage code once, and it works everywhere.
This is a pretty cool system because it doesn’t limit us to local device storage. We can have device classes that talk to Google-Storage, One-Drive, Dropbox and so on. It also opens up for custom storage solutions should you already have this pre-made on your server.
Database support, a quick overview
Databases have always been available in Smart Mobile Studio. We have units for WebSQL, IndexDB and SQLite. In fact, we even compiled SQLite3 from native C code to asm.js, meaning that the whole database engine is now pure JavaScript and no-longer dependant on W3C standards.
Besides these we also have TW3Dataset which is a clean, Smart Pascal implementation of a single table dataset (somewhat inspired by Delphi’s TClientDataset). In our previous beta we upgraded TW3Dataset with a robust expression parser, meaning that you can now set filters just like Delphi does. And its all written in Smart Mobile Studio which means there are no dependencies.
And ofcourse, there is also direct connections to Embarcadero Datasnap servers, and Remobjects SDK servers. This is excellent if you have an already existing Delphi infrastructure.
A unified DB framework
If you were hoping for a universal DB framework in beta-2 of v3.0, sadly that will not be the case. The good news is that databases should make it into v3.2 at the latest.
Databases looks simple: table, rows and columns right? But since each database engine known to JavaScript is written different from the next, our model has to take height for these and be dynamic enough to deal with them.
The model we used with WebSQL is turning out to be the best way forward I feel, but its important to leave room for reflection and improvements.
So getting our DB framework established is a priority for us, and we have placed it on our timeline for (at the latest) v3.2. But im hoping to have it done by v3.1. So it’s a little ahead of us, but we need that time to properly evolve the framework.
Smart Desktop [a.k.a Amibian.js]
The feedback we have received on our Smart Desktop demos have been pretty overwhelming. It is also nice to know that our prototype is being used to deliver software to schools and educational centers. So our desktop is not going away!
But we are not rushing into this without some thought first. The desktop will become a project type like I have written about many times before. So you will be able to create both the desktop and client applications for it. The desktop is suitable for software that requires a windowing environment (a bit like Sencha or similar frameworks). It is also brilliant for kiosk displays and as a remote application hub.
Our new storage device system came largely from Amibian, and with these now a part of our RTL we can clean up the prototype considerably!
Smart assembler
It may sound like an oxymoron, but a lab project we created while testing our parser framework (system.text.parser unit) turned into an exercise in compiler / assembler making. We implemented a virtual machine that runs instructions represented by bytecodes (fairly straight ahead stuff). It supports the most common assembler methods, vaguely inspired by the Motorolla 68k processor with a good dose of ARM thrown in for good measure.
If you ponder why on earth this would be interesting, consider the following: most web platforms allow for scripting by third-party developers. And by opening up for that these, the websites themselves become prone to attacks and security breaches. There is no denying that any JS based framework is very fragile when potentially hundreds of unknown developers are hacking away at it.
But what if you could offer third parties to write plugins using more traditional languages? Perhaps a dialect of pascal, a subset of basic or perhaps C#? Wouldnt that be much better? A language and (more importantly) runtime that you have 100% control over.
While our assembler, disassembler and runtime is still in its infancy (and meant as a demo and excercise), it has future potential. We also made the instructions in such a way that JIT compiling large chunks of it is possible – and the output (or codegen) can be replaced by for example web assembly.
Right now it’s just a curiosity that people can play with. But when we have more time I will implement high-level parsers and codegens that emit code via this assembler. Suddenly we have a language that runs under node.js, in the browser or any modern JS runtime engine – and its all done using nothing but Smart Mobile Studio.
Well, stay tuned for more!
Websocket, WebSocket.io and Socket.io
Wow. Thats is a mouthfull isn’t it? Websocket, websocket.io and socket.io. So what is that all about?
In short, im finishing off the server units for Smart Mobile Studio! And I think people are going to be super pleased and happy about this one.
As you may know, node.js gives your JavaScript more or less the same functionality as classical programming languages. You get to work with sockets, talk to databases, implement protocols – all of it in JavaScript and all of it capable of doing exactly what we are doing in Delphi. That is pretty neat!
But that also means someone has to expose the node.js functionality in pascal form and then write thin wrappers around that. This is where Smart Mobile Studio really shines, because after six years of constant development – we have built up a very powerful codebase. Smart mobile studio is not just about compiling to JavaScript. There are 10-20 compilers out there doing that. Nope, the power of smart mobile studio is in the infrastructure we have painstakingly constructed for our customers. Hundreds of hand-written classes, units and library files. Add to that our cosy little IDE which is very nice to work with and the advantages starts piling up.
Servers, glorious node.js servers
So whats on the menu? A lot! More than I actually thought we would add in the next update. Here is a list of the server types you will be getting:
- http server
- websocket server
- tcp/ip server
- socket.io server
- websocket.io server
To explain the differences:
Websocket is a full-duplex communication stack that allows both client and server to talk; at the same time. It also contains fancy commands for emitting messages to all connected clients, and likewise a client can commit messages to other clients (if the server allows it). You can send both text messages and binary messages. And the cool part is that the websocket engine will collect the data, deal with net-frames and spare you all the razzle you would otherwise have to do.
On top of this we have something called socket.io. This takes the whole thing one step further, allowing you to define a set of commands on the server (by name), and associate that with the code to execute when such a command is received. Socket.io also come in client-form for both browser and node.js itself. And just like the server edition you can define a set of commands for the client as well. This moves websocket into the realm of real-time messaging, where rather than wasting days and weeks coding your own protocol, you can kick back and flesh out your command-set much in the way RemObjects SDK does. Socket.io also contains clustering mechanics, but I wont cover that just yet.
Websocket.io is sort of third option. It’s basically websocket and socket.io glued together. You don’t have to upgrade the http socket (read up on websocket if you have no clue what that means) if you don’t want to. Websocket.io gives you pretty much exactly the same as websocket + socket.io, but from a unified API. And to be honest this is the library I use the most myself.
So long story short: you get 3 websocket based server classes to play with, all of them inheriting from TNJCustomServer, so you use the same code to work with all servers. And the speed is phenomenal! I have to pinch myself sometimes just to remember that this is kick-ass javascript running the show.
If you think websocket is “communication with training wheels” (I don’t, I frikkin love it) then a raw TCP server is probably your cup of tea. This is a perfect class to use if you need to port over some old Delphi code, if you for some reason hate XML or JSON – or perhaps you just want to dump over raw binary data as quickly as possible. Either way: it’s there, and it works brilliantly.
As you would expect I’m also doing the client side of things. We have supported websocket out of the box for a while now, and I just added a socket.io client to the codebase. I have a few alternatives to try out on the raw tcp stuff, but ultimately tcp is now allowed for HTML5. But a client and server for node.js is ofcourse on the menu.
Benefits
It’s when I sit and work with the codebase like I do now that I see just how much cool stuff we have. When doing research on a library, how to best wrap or implement it in our RTL, I naturally come across hundreds of comments by JavaScript programmers. Things like allocating memory, copying data from one buffer to the next, working with bytes and bits — a lot of these guys are having a really hard time doing classical programming.
But for Smart Pascal that’s not even an issue. We have supported memory allocation, blue-pointers and even a threading model for quite some time now. Yes, threading. Check your RTL folder and you will find it 🙂 It goes without saying that when you have memory streams, file streams, raw buffer classes, stacks, queues and proper inheritance that smart pascal gives you more than just a head-start. The benefits are astronomical in places.
And no, im not just blowing my own horn here — I use Smart Mobile Studio to create services and mobile applications on a daily basis. So I am painfully aware of what we did wrong, but also what we did right. And thankfully we got more right than wrong.
Smart Mobile Studio, updates ohoy
Its been a while since I’ve written much about Smart Mobile Studio. There are many reasons for this, but I figured I could write a bit now so people dont think we are standing still 🙂 So.. whats going on? Lets go through the highlights!

Some of the more advanced demos include a full bytecode compiler written in Smart Mobile Studio. It may seem like overkill, but its a perfect example of just what Smart can deliver!
New team
When you have a small team you can pivot on a dime. There are no board meetings, no finding dates that suits everyone, “vacation” is a foreign word – and you can pretty much jump on ideas and just implement them straight away.
But as software grows in size and complexity, that way of working can cause more challenges than solutions. You have to establish a proper company structure, plan ahead with care, delegate tasks and do chores you may not like.
Also, teams are people. And people change, take on new jobs and move. Without much drama that is the case for us as well — and while the core of the old team is still working on Smart, we had to establish a new team with more members (and board meetings and .. well, you know how it goes).
New RTL structure
The initial RTL was built side by side with the compiler. In the beginning we didnt even have classical functions like Assigned(), so the codebase had a lot of esoteric twists and asm sections that – as the compiler grew in complexity, became redundant and caused hard to track-down bugs. The compiler is now en-par with Delphi in many ways, although without generics; It represents a whole new dialect of pascal. Where Delphi is the absolute top-dog when it comes to Windows desktop applications – tailored from scratch to fit the Windows API, Smart Pascal was made for Javascript and to interact with the javascript virtual machine environment.
But the old structure, while it served us well in knocking out Cordova Phonegap based apps quickly — just had to be refactored at some point. Javascript is Async by nature and trying to “mimic” blocking execution was starting to impact performance, order and even being able to read and understand the code (for users coming from Delphi or Freepascal).

Smart Mobile Studio Unleashed will be released before xmas 2016, it explains in great detail everything you need to write controls, work with memory, write services and basically use Smart Mobile Studio to its full potential.
Just think about it: We want to support mobile devices, nodejs client/server, embedded technology, Windows Universal apps and all the exciting gadgets that are coming out with pure JS interfaces. To pull that off you really need all your ducks in order and a clear separation between visual. non visual and hybrid systems must be in place.
We have solved this quite elegantly in my opinion, by the use of namespaces.
You have the system namespace which implements platform independent code (and abstract classes). Then you have the SmartCL visual framework that contains code that targets the DOM and browser. And finally you have the SmartNJ namespace that contains code adapted for NodeJS and headless frameworks. For embedded devices each will have their own namespace (like Arduino), all of them rooted in the platform independent system namespace.
This is an elegant way of solving how you approach the different platforms out there, it has less overhead than the “all in one” VCL type RTL we have fostered so far.
The downside is naturally, like C# coders live with every day, more units. But I have made sure that correlations match 1:1 as much as possible.
So where you find, for example, “system.filesystem.pas”, you will also find a corresponding “smartcl.filesystem.pas” and a “smartnj.filesystem.pas”. So once you know the units in the system namespace, its just a matter of replacing the prefix for the platform you target.
New tech
While re-factoring whole RTL is more than enough work, we also want to get new tech into Smart Mobile Studio. A unified filesystem has been long coming, and while we have units to access pretty much every nook and crumble Javascript has to offer — a high level interface that is unified is really where my focus is now.
Storing files on mobile devices, in the browser, node or embedded really should be the same. You should write code once and it should work no matter what namespace you target. I want to shield users from some of the Javascript solutions still out there. But people should always have the option to dig into the underlying API.
Databases
Then there is databases. This has really baked my noodle. As mentioned JavaScript is not linear. Its Async by nature, which means writing a concrete API that fits all has been problematic to say the least. But I am happy to say that the code I have implemented is finally rich enough to capture all of them.

The long awaited DB grid. This one handles thousands of records very fast and uses CSS GPU scrolling and effects. As always its skinnable through the stylesheet
But a DB API is just the foundation. NodeJS has drivers for almost all native databases out there, like MSSQL, MariaDB, Oracle and even good old access for that matter. Implementing these under a unified framework takes time — which is also why we needed more people to join the group.
I have covered the most essential, the built in databases supported by the browser, sqlite and also TW3Dataset — this is more than enough for mobile and browser applications. But NodeJS is a whole other ballgame, and that has really been a challenge. Allthough a very rewarding one intellectually. But NodeJS is so massive that we have to build up over time, its impossible for such a small team to deliver high-level classes for all of them in such short time.
Actions
Actions is another factor I have really wanted for a long time. In Delphi and Lazarus this makes programming so much easier, faster and elegant. And writing a Smart version of this was not hard at all. So this is in place — we only need to adjust the property inspector and automatic codegen to take height for action objects.
Tweening
The effect system in Smart is pretty cool. In fact its one of the most powerful CSS GPU based systems out there. No other system makes writing GPU powered effects so easy, yet its one of the least known aspects of Smart Mobile Studio.

Momentum scrolling for listboxes and displays is now standard. In fact, the actual scroll code is isolated in components, so you can mix and match the scroll type you like.
The downside of effects that uses hardware, is that they have to run their course. Once started you cant really stop them, or abort the sequence mid-way. This is actually down to the browser, and even though its all defined in the WC3 standards – no browser really implements the stop feature.
To give users an alternative we have implemented a CPU only tweening system. This can be used to tween (morph is also a word used for this) anything from colors to moving elements. But as mentioned, CPU will always be slower than GPU. But with the tweening library in place, used for example in combination with Sprite3d — you have everything you need to write high-speed, silky smooth multimedia and control effects.
Components, not just controls
And yes, finally we have a pure separation between visual and non-visual components. Once established in the IDE you will be able to drag & drop non-visual controls, and we have debated the use of datamodules as well.
Since the VCL/LCL TComponent type inheritance chain is quite fat, we have avoided that. Non visual controls are called widgets, withe TW3Component now inheriting from TW3CustomWidget. This makes the inheritance chain linear – but allows you to safely write non visual components that are universal and dont collide with the namespaces scheme.
This is really cool! For instance listboxes now have a scrollmethod property where you can hook up a non-visual scroll mechanism. Momentum scroll? Row-by-Row scroll? Flat scroll? It makes it so much easier to hook things up.
Where to next?
There are a few company formalities left, but once those are in place we’ll get cracking! I personally cant wait to get going on the new IDE features. And with nodeJS becoming more and more relevant, both for embedded work and Smart devices – I think everyone will agree that it was worth the wait!
And as always — a long, long nose to those that said “it cant be done” 🙂
For those about to scroll, we salute you
Came up with a better solution to the scrolling problem for Smart Mobile Studio. While I hate having to write code for Internet Explorer, it is nice to have a system that works everywhere. But that means more abstraction and “drivers” type classes.
The scrolling model
TW3Scrollbox and it’s variation(s) implement different types of scrolling. TW3Scrollbox has a very fast non-momentum scroll, making it perfect for displaying detailed information. But it would be nice if we could choose right?
The model I have come up with is super simple, especially in the upcoming RTL where we finally have non-visual components. I give you – TW3ScrollController
In the present model, TW3Scrollbox deals with scrolling directly. Actually the scrolling is implemented in the TW3ScrollContent control, mapping touch and mouse events directly. This turned out to be a remarkably fast way of moving things around. I really did not expect IE to keep up, but it’s perfectly pristine!
In the momentum-scroll example I posted, control of scrolling is handled by the container rather than content. This is very fast on webkit, and I also tried it on Microsoft mobile and Windows 10 mobile – and it’s very fast there. But for some reason the desktop Internet Explorer is slow and the content jitters a bit.
The culprit is not my code or approach, it’s actually something else. Internet Explorer and Edge are the only browsers that implements OnReSize() events. No other browser has this. In the SMS RTL we have to manually figure out when to call ReSize (based on the BeginUpdate, AddControlState, EndUpdate methods).
In the momentum scroller I use SetBounds() to keep the content within the horizontal bounds of the control. This causes an extra call to Resize every time the content moves even a pixel (even though it shouldnt, because the size doesnt change). So yeah, fixing that will make all the difference. I’m going to nail this thing once and for all, just like I did with font measurements way back.
TW3ScrollController
But isolating the code that actually deals with scrolling in separate, non visual components that you can attach to the TW3ScrollBox makes sense. Rather than hardcoding everything into a huge, spaghetti monster unit — we can now isolate different scroll methods in their own units (keeping those bytes down).
Normal per-pixel scrolling, momentum scrolling, CSS3 animation based scrolling, tween based (cpu) scrolling. It gives us some options – and allows you to implement your own variations if you find mine lacking.
I’m also giving the browser driver a much deserved overhaul. Getting the browser type and version info should be easy (and humanly readable). And since you may want to pick different scroll methods depending on the browser type — being able to check if your running on Edge for the desktop, or IE on a mobile device… well, it should be there. End of story.
iScroll
I really want iScroll to be the standard scroll library for Smart Mobile Studio, but since people feel it’s hard to use and adapt to — I may end up doing the unthinkable and re-write it in object pascal from scratch. But iScroll5 really is so much better. It has been developed and tested on a plethora of devices for six years now.
It even does things the built-in browser scrolling (for the browsers that allows this, yeah im looking at you Safari!) doesn’t deal with.
But I have enough on my plate right now, so iScroll porting will have to wait.
Momentum Scrolling
Momentum scrolling is something we havent had as an option in the VJL directly. We excluded it initially because there were excellent JavaScript libraries especially for this (like iScroll), but in retrospect I guess it wouldnt hurt to have it in the VJL written in object pascal.
Here is a little something I slapped together the other day. Im going to make both TListbox and the ordinary content containers have an option for this.

Oooo.. sexy sexy scroller thingy!
Note: This supports both mouse and touch, and if you are confused about the event objects then head over to Github and snag a copy of that there. Just remove the references to units you dont have and include eventobjs.pas in your uses clause!
The call to SetInitialTransformationStyles() should be replaced with (this makes the browser mark the element for GPU, which is very fast):
FContent.Handle.style[BrowserAPI.Prefix('transformStyle')] := 'preserve-3d'; FContent.Handle.style[BrowserAPI.Prefix('Perspective')] := 800; FContent.Handle.style[BrowserAPI.Prefix('transformOrigin')] := '50% 50%'; FContent.Handle.style[BrowserAPI.Prefix('Transform')] := 'translateZ(0px)';
Oh and it fades out the indicator after a scroll session, quite nice if I say so myself 🙂
Enjoy!
unit Form1; interface uses System.types, System.Colors, System.Events, System.Time, System.Widget, System.Objects, W3C.Date, W3C.DOM, SmartCL.Effects, SmartCL.Events, SmartCL.MouseCapture, SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms, SmartCL.Fonts, SmartCL.Borders, SmartCL.Application, SmartCL.Controls.Listbox, SmartCL.Controls.Panel, SmartCL.Controls.CheckBox, SmartCL.Controls.Button; type TScrollContent = class(TW3CustomControl) end; TW3ScrollIndicator = class(TW3CustomControl) end; TW3VScrollControl = class(TW3CustomControl) private FYOffset: integer; FContent: TScrollContent; FVRange: TW3Range; FHRange: TW3Range; FPressed: boolean; FStartY: integer; FTarget: integer; FAmplitude: double; FTimestamp: integer; FVelocity: double; FFrame: double; FTicker: TW3DispatchHandle; FFader: TW3DispatchHandle; FTimeConstant: double; FMouseDownEvent: TW3DOMEvent; FMouseUpEvent: TW3DOMEvent; FMouseMoveEvent: TW3DOMEvent; FTouchDownEvent: TW3DOMEvent; FTouchMoveEvent: TW3DOMEvent; FTouchEndsEvent: TW3DOMEvent; FIndicator: TW3ScrollIndicator; function GetYPosition(const E: variant): integer; procedure MoveBegins(sender: TObject; EventObj: JEvent); procedure MoveEnds(sender: TObject; EventObj: JEvent); procedure MoveUpdate(sender: TObject; EventObj: JEvent); procedure HandleContentSizeChanged(sender: TObject); protected procedure Track;virtual; procedure AutoScroll;virtual; procedure ScrollBegins;virtual; procedure ScrollEnds;virtual; procedure Resize;override; procedure InitializeObject; override; procedure FinalizeObject; override; procedure ObjectReady;override; procedure ScrollY(const NewTop: integer); public Property Content:TScrollContent read FContent; end; TForm1 = class(TW3Form) procedure W3Button1Click(Sender: TObject); private {$I "Form1:intf"} FBox: TW3VScrollControl; protected procedure InitializeForm; override; procedure InitializeObject; override; procedure Resize; override; end; implementation //################################################################### // TW3VScrollControl //################################################################### procedure TW3VScrollControl.InitializeObject; begin inherited; FPressed:=false; FYOffset := 0; FStartY := 0; FTimeConstant := 325; Background.fromColor(clWhite); FContent := TScrollContent.Create(self); FIndicator:=TW3ScrollIndicator.Create(self); FIndicator.width:=8; FIndicator.height:=32; FIndicator.StyleClass:='TW3ScrollContentIndicator'; FIndicator.Transparent := true; FMouseDownEvent := TW3DOMEvent.Create(self); FMouseDownEvent.Attach("mousedown"); FMouseDownEvent.OnEvent := @MoveBegins; FMouseMoveEvent := TW3DOMEvent.Create(self); FMouseMoveEvent.Attach("mousemove"); FMouseMoveEvent.OnEvent := @MoveUpdate; FMouseUpEvent := TW3DOMEvent.Create(self); FMouseUpEvent.Attach("mouseup"); FMouseUpEvent.OnEvent := @MoveEnds; FTouchDownEvent := TW3DOMEvent.Create(self); FTouchDownEvent.Attach("touchstart"); FTouchDownEvent.OnEvent:= @MoveBegins; FTouchMoveEvent := TW3DOMEvent.Create(self); FTouchMoveEvent.Attach("touchmove"); FTouchMoveEvent.OnEvent := @MoveUpdate; FTouchEndsEvent := TW3DOMEvent.Create(self); FTouchEndsEvent.Attach("touchend"); FTouchEndsEvent.OnEvent := @MoveEnds; FContent.Handle.ReadyExecute( procedure () begin (* Mark content for GPU acceleration *) FContent.SetInitialTransformationStyles; end); end; procedure TW3VScrollControl.ObjectReady; begin inherited; FContent.OnReSize := HandleContentSizeChanged; FIndicator.left:=ClientWidth-FIndicator.width; FIndicator.bringToFront; FIndicator.Visible:=false; resize; end; procedure TW3VScrollControl.FinalizeObject; begin FContent.free; inherited; end; procedure TW3VScrollControl.HandleContentSizeChanged(sender: TObject); begin if not (csDestroying in ComponentState) then begin FVRange := TW3Range.Create(0, FContent.Height - ClientHeight); FHRange := TW3Range.Create(0, FContent.Width - ClientWidth); end; end; procedure TW3VScrollControl.Resize; var LClient: TRect; begin inherited; if (csReady in ComponentState) then begin LClient := ClientRect; FVRange := TW3Range.Create(0, FContent.Height - LClient.Height); FHRange := TW3Range.Create(0, FContent.Width - LClient.Width); FContent.SetBounds(0,FContent.top,LClient.Width,FContent.height); FIndicator.MoveTo(ClientWidth-FIndicator.Width,FIndicator.top); end; end; procedure TW3VScrollControl.ScrollY(const NewTop: integer); var LGPU: string; LIndicatorTarget: integer; function GetRelativePos:double; begin result := (ClientHeight - FIndicator.Height) / (FContent.Height - ClientHeight); end; begin if not (csDestroying in ComponentState) then begin if (csReady in ComponentState) then begin (* Use GPU scrolling to position the content *) FYOffset := FVRange.ClipTo(NewTop); LGPU := "translate3d(0px,"; LGPU += FloatToStr(-FYOffset) + "px, 0px)"; FContent.Handle.style[BrowserAPI.Prefix("Transform")] := LGPU; (* Use GPU scrolling to position the indicator *) LIndicatorTarget := FYOffset * GetRelativePos; FIndicator.left := clientwidth - FIndicator.width; LGPU :="translateY(" + TInteger.ToPxStr(LIndicatorTarget) + ")"; FIndicator.Handle.style[BrowserAPI.Prefix("Transform")]:= LGPU; end; end; end; procedure TW3VScrollControl.Track; var LNow: integer; Elapsed: integer; Delta: double; V: double; begin LNow := TW3Dispatch.JsNow.now(); Elapsed := LNow - FTimestamp; FTimestamp := TW3Dispatch.JsNow.now(); Delta := FYOffset - FFrame; FFrame := FYOffset; v := 1000 * Delta / (1 + Elapsed); FVelocity := 0.8 * v + 0.2 * FVelocity; end; procedure TW3VScrollControl.ScrollBegins; begin TW3Dispatch.ClearInterval(FFader); if not (csDestroying in ComponentState) then begin FIndicator.Visible := true; FIndicator.AlphaBlend := true; FIndicator.Opacity := 255; end; end; procedure TW3VScrollControl.ScrollEnds; begin TW3Dispatch.ClearInterval(FFader); if not (csDestroying in ComponentState) then begin FFader:=TW3Dispatch.SetInterval(procedure () begin FIndicator.AlphaBlend := true; FIndicator.Opacity := FIndicator.Opacity - 10; if FIndicator.Opacity=0 then begin TW3Dispatch.ClearInterval(FFader); end; end, 50); end; end; procedure TW3VScrollControl.AutoScroll; var Elapsed: integer; Delta: double; begin if FAmplitude<>0 then begin Elapsed := TW3Dispatch.JsNow.now() - FTimestamp; Delta := -FAmplitude * Exp(-Elapsed / FTimeConstant); end; (* Scrolled passed end-of-document ? *) if (FYOffset >= (FContent.Height - ClientHeight)) then begin TW3Dispatch.ClearInterval(FTicker); FTicker := unassigned; ScrollY(FContent.Height-ClientHeight); ScrollEnds; exit; end; (* Scrolling breaches beginning of document? *) if (FYOffset < 0) then begin TW3Dispatch.ClearInterval(FTicker); FTicker := unassigned; ScrollY(0); ScrollEnds; exit; end; if (delta > 5) or (delta < -5) then begin ScrollY(FTarget + Delta); W3_RequestAnimationFrame(AutoScroll); end else begin ScrollY(FTarget); ScrollEnds; end; end; function TW3VScrollControl.GetYPosition(const e: variant): integer; begin if ( (e.targetTouches) and (e.targetTouches.length >0)) then result := e.targetTouches[0].clientY else result := e.clientY; end; procedure TW3VScrollControl.MoveBegins(sender: TObject; EventObj: JEvent); begin FPressed := true; FStartY := GetYPosition(EventObj); FVelocity := 0; FAmplitude := 0; FFrame := FYOffset; FTimestamp := TW3Dispatch.JsNow.now(); TW3Dispatch.ClearInterval(FTicker); FTicker := TW3Dispatch.SetInterval(Track,100); EventObj.preventDefault(); EventObj.stopPropagation(); end; procedure TW3VScrollControl.MoveUpdate(sender: TObject; EventObj: JEvent); var y, delta: integer; begin if FPressed then begin y := GetYPosition(eventObj); delta := (FStartY - Y); if (Delta>2) or (Delta < -2) then begin FStartY := Y; ScrollY(FYOffset + Delta); end; end; EventObj.preventDefault(); EventObj.stopPropagation(); end; procedure TW3VScrollControl.MoveEnds(sender: TObject; EventObj: JEvent); begin FPressed := false; TW3Dispatch.ClearInterval(FTicker); if (FVelocity > 10) or (FVelocity < -10) then begin FAmplitude := 0.8 * FVelocity; FTarget := round(FYOffset + FAmplitude); FTimeStamp := TW3Dispatch.JsNow.Now(); ScrollBegins; w3_requestAnimationFrame(autoscroll); end; EventObj.preventDefault(); EventObj.stopPropagation(); end; { TForm1 } procedure TForm1.W3Button1Click(Sender: TObject); begin self.FBox.Content.height:=1000; end; procedure TForm1.InitializeForm; begin inherited; // this is a good place to initialize components FBox := TW3VScrollControl.Create(self); FBox.SetBounds(10,10,300,300); // var LText :=" <table cellpadding=|0px| style=|border-collapse: collapse| width=|100%|>"; for var x:=1 to 400 do begin if ((x div 2) * 2) = x then LText += " <tr padding=|0px| style=|border: 0px solid black; background:#ECECEC|>" else LText += " <tr style=|border: 0px solid black; background:#FFFFFF|>"; LText += " <td padding=|0px| height=|32px| style=|border-bottom: 1px solid #ddd|>" + x.toString + "</td> "; LText += " <td style=|border-bottom: 1px solid #ddd|>List item #" + x.toString + "</td> "; LText += "</tr> "; end; LText +="</table> "; LText := StrReplace(LText,'|',''''); FBox.Content.innerHTML := LText; FBox.Content.width:=1000; FBox.Content.height := FBox.Content.ScrollInfo.ScrollHeight; end; procedure TForm1.InitializeObject; begin inherited; {$I "Form1:impl"} end; procedure TForm1.Resize; begin inherited; if (csReady in ComponentState) then begin //FBox.setBounds(10,10,clientwidth div 2, clientHeight div 2); end; end; initialization begin Forms.RegisterForm({$I %FILE%}, TForm1); end; end.
TW3Dataset, Smart Data and what you can do
In my previous post, I have requested some input from users about what they would like in the future of SMS. The question of the hour being what features or architecture would you guys like to have regarding databases and data-bound controls?
In this short post I want to write some words about what is already in Smart Mobile Studio and what you can do right now. I have also added the source code for a CSS3 powered DB grid (98% finished) that you can play with. So plenty of things in the pipeline for Smart Mobile Studio. Right, let’s get cracking and have a look at whats on the menu!
TW3Dataset
Delphi has one component which is completely and utterly unappreciated; and that is TClientDataset. This really is a fantastic piece of code (really, hear me out). First, it was written to be completely datatype agnostic and stores raw values as variants. That in itself is a monumental achievement considering that Delphi’s variants are binary compatible with Microsoft COM variants (read: sluggish, slow, and a proverbial assault on the CPU stack). Secondly, it makes use of variant-arrays to pack columns into a portable, single variant record. The author should be given a medal for not commiting suicide while creating that component. And this is just scratching the surface of TClientDataset, it really is awesome once you get the hang of it. It can act as a stand-alone, in memory only, table. It can act as a intermediate local data cache, keeping track of record changes, tagging, reverts and everything under the sun – before pushing it to a master data provider. So you can locally work with a database on the other side of the world, caching up changes and then push the whole changeset in one go.
TClientDataset is so confusing and had so much potential that Cary Jensen sat down and wrote a full 350 page book on the subject. A lot of Delphi and FreePascal developers just laugh when they hear someone mention TClientDataset, but that component is a gem if you know how to use it properly.
What about Smart?
Smart Mobile Studio ships with wrappers for WebSQL only. IndexDB will be included in the next update, but I seriously advice against using it unless you are going to package your final product with PhoneGap. Both WebSQL and IndexDB represents an onslaught of callback events, to the point of being useless in anything but JavaScript.
Only experienced and advanced Smart developers will be able to master these beasts. I have done my best to simplify their use, but I must admit I hardly ever use these myself. WebSQL is “OK” to use when you package your app with Phonegap; that takes away the 5Mb limitation and extends the procedure execution time — but other than that, all my large apps use TW3Dataset for storage.
Right. Let’s sum up what Smart Mobile Studio has to offer right now when it comes to databases and data transportation:
WebSQL
WebSQL is a small and compact SQL database. Most browsers include SQLite and simply expose that through JavaScript. In Smart Mobile Studio WebSQL is encapsulated as classes and actions in the unit SmartCL.DbSQL.pas. While WebSQL is by far the best HTML5 database, please note that it’s been marked as deprecated (meaning: it will be phased out and removed in the future). Also, it has a limitation of 5-10 megabytes per database. WebSQL is excellent if you use PhoneGap to package your final product (and you know your way around JavaScript to begin with). Phonegap removes the storage limitation and also extends the JSVM execution limitation (a procedure call must return within 2 seconds), allowing you to operate with large and complex queries.
DataSnap
Datasnap is a Delphi technology for exposing databases and RPC (remote procedure call) services to the world. Datasnap clients can call datasnap servers to obtain data, invoke methods and so on. Datasnap is an excellent way of re-using your Delphi back-end services with HTML5 or mobile applications. Smart Mobile Studio supports DataSnap out of the box, so if your company has existing datasnap databases available, your Smart applications can connect and make use of them.
It must however be mentioned that third party solutions like Mormoth offer much better performance and stability than Datasnap. Mormoth also supports Smart Mobile Studio.
Remobjects SDK
While not really a DB framework, Smart Mobile Studio can connect and invoke RO services. This opens up for some very exciting possibilities, and pushing data over an RPC framework is not hard to do. It’s also a great way to re-cycle existing RO based native services with your HTML5 or mobile applications. Smart Mobile Studio can import RO service libraries and generate Smart Pascal client interfaces for you.
TW3Dataset
This is a single table database engine written in Smart Pascal itself. It has no support for SQL or filtering (as of writing) but also no limitation on size. File storage under JSVM is however limited to the same 5-10 megabyte restriction as WebSQL, but the limitations mentioned are removed by PhoneGap. Since PhoneGap is what you want to use in getting your product onto AppStore, Google Play or Android Marketplace, the storage limitation has no real impact.
Working with TW3Dataset
Tw3Dataset was designed to be Smart Mobile Studio’s version of TClientDataset for Delphi (read: inspired but less complex) and it will no doubt grow as we establish our DB framework in 2016. You can use it to keep track of local data changes, but you have to reserve a field for that information yourself. TW3Dataset is simply a small, in memory, to the point dataset which is perfect for applications that doesnt generate huge amounts of data. It should also remain as small and compact as possible because it acts as a building-block for more complex components.
Why is this useful? Well consider this as an example: Developer Express, a great company offering a wide variety of components, sell components that mimic and implement Microsoft Outlook. You have the calendar, the day planner, the vertical scrolling menu system, the freestyle note editing; DevEx have more or less reverse engineered the visual components that make up Microsoft Outlook. The downside? Well, with such a complex, inter-connected component set, the information it generates and depends on is equally complex! So DevEx allows you to store the data directly to an existing database. They also provide a drop-in solution, in-memory tables that are created and maintained by the components for you. This is a perfect situation where a TW3Dataset would be handy to use. Rather than exposing a ton of storage events, so many that it overwhelms the developer — the components can deal with all of that and just give you a handy way of loading and saving that data.
This is the idea behind TW3Dataset. It was designed not to be complex, support SQL or any advanced features. It should be simple so people can use it to create large and complex components that can export and import data in a uniform way.
Creating a table
Before we create a table, we first have to define what the table looks like. This is done by populating the field-definitions property. TW3Dataset supports generated fields, so you can have both AutoInc and GUID fields which are generated automatically. When you have populated the field-definistions, we simply call CreateDataset() to establish the table.
var LDataset: TW3Dataset; LDataset.FieldDefs.Add('id',ftAutoInc); LDataset.fieldDefs.Add('firstname',ftString); LDataset.fieldDefs.Add('lastname',ftString); LDataset.fieldDefs.add('text',ftString); LDataset.CreateDataset;
Adding records
Adding records to the dataset is straight forward and more or less identical to how you would do it under Delphi. You have both append and insert operations. Let’s use Append for this example:
var x: Integer; LDataset: TW3Dataset; LDataset.FieldDefs.Add('id',ftAutoInc); LDataset.fieldDefs.Add('firstname',ftString); LDataset.fieldDefs.Add('lastname',ftString); LDataset.fieldDefs.add('text',ftString); LDataset.CreateDataset; for x:=1 to 10 do begin LDataset.Append; LDataset.Fields.FieldByName('firstname').AsString := 'John'; LDataset.Fields.FieldByName('firstname').AsString := 'Doe'; LDataset.Fields.FieldByName('text').AsString:='This is #' + x.toString; LDataset.Post; end;
As you can see from the code above, calling the Append() method sets the dataset in insert mode. This means it allocates the memory needed to hold the record, generates the automatic values (ID autoinc in this case) and allows access to the fields object. If you try to alter values without being in Insert or Edit mode, an exception is raised. This is standard for most languages so nothing new here. The Post() method commits the record buffer to the table, storing it in memory.
Navigating the data
Navigation is done via First, Last, Next, Back methods. You can also check for eof-of-file and beginning-of-file via traditional BOF and EOF properties. So let’s traverse the dataset we just created and dump the output to the console!
var x: Integer; LDataset: TW3Dataset; LDataset.FieldDefs.Add('id',ftAutoInc); LDataset.fieldDefs.Add('firstname',ftString); LDataset.fieldDefs.Add('lastname',ftString); LDataset.fieldDefs.add('text',ftString); LDataset.CreateDataset; for x:=1 to 10 do begin LDataset.Append; LDataset.Fields.FieldByName('firstname').AsString := 'John'; LDataset.Fields.FieldByName('firstname').AsString := 'Doe'; LDataset.Fields.FieldByName('text').AsString:='This is #' + x.toString; LDataset.Post; end; LDataset.first; while not lDataset.EOF do begin var id := LDataset.fields.fieldbyname('id').asString; var txt := LDataset.fields.fieldbyname('text').asString; writeln(id + ' ' + txt); LDataset.Next; end;
And here is the output in the console:
Loading and saving
TW3Dataset allows you to save your data to a normal string or a stream. You may remember that in our last update we added TStream support as well as the possebility to allocate and work with raw memory? Well, TW3Dataset makes storage very simple. Since it exports ordinary JSON in text format, you can also use TW3Dataset as an intermediate format. It’s small enough (depending on the number of records) to be pushed to a server, and also a convenient format for retrieving X number of records from a server.
For storing datasets locally, in the browser or on your phone, just use TLocalStorage and stuff the data there. Just be aware of the limitation your browser impose on you when not running under PhoneGap (max 5-10 megabyte, the limit toggles between these depending on browser type and build number).
function SaveToString:String; Procedure LoadFromString(Const aText:String); Procedure SaveToStream(const Stream:TStream);virtual; Procedure LoadFromStream(const Stream:TStream);virtual;
Grids
This has been somewhat missing in Smart Mobile Studio. Like mentioned in my previous article, we are still working on a “final” framework for databases under Smart, and a grid cannot really be created before you have some data it can bind to. But, I have actually a grid that may be of interest. It’s actually been lying around my PC since june 2015. I’m going to publish the source for this on Github later, and you can use that until we finalize the DB framework.
It makes full use of HTML5 hardware scrolling, effects and more. It’s also heavily adaptable, so you can use CSS3 animations on rows – or transform rows into something else (like clicking a row and having the row transform into an editor). It’s pretty neat! But I still need to clean it up a bit. And there is a handfull of features that must be added to the RTL before it can be used by everyone. Here is a picture of it. It doesnt capture the CSS3 animations or the animated column-dragging, but its pretty neat 🙂
If you want to play around with the source, here you go:
unit smartCL.dbgrid; interface uses System.Types, system.types.convert, System.colors, SmartCL.system, SmartCL.Components, SmartCL.Controls.Label, SmartCL.Fonts, SmartCL.Borders, SmartCL.Controls.ScrollBar; type TEdgeSenseControl = partial class(TW3Label); TGridRowContainer = partial class(TW3CustomControl); TCustomGrid = partial class(TW3CustomControl); TEdgeRegions = (scLeft,scTop,scRight,scBottom,scNone); TEdgeSenseEdges = set of TEdgeRegions; (* TEdgeSenseControl: This control checks its own edges and changes mouse cursor accordingly. The edges to check is defined by the SenseEdges pascal SET. Use setSenseEdges() to define what edges to check for. The active edge (hovered by the mouse-pointer) is reflected in ActiveEdge To get the X/Y offset of the pointer inside an edge zone, call getActiveEdgeOffset() Edge sensebility can be disabled and enabled with DisableSense() and EnableSense(). *) TEdgeSenseControl = partial Class(TW3Label) const CNT_LEFT = 0; CNT_TOP = 1; CNT_RIGHT = 2; CNT_BOTTOM = 3; CNT_SIZE = 10; private FEdges: TEdgeSenseEdges; FRects: Array[0..3] of TRect; FEdgeId: Integer; FSense: Boolean; procedure CheckCorners(const x,y:Integer); procedure UpdateCursor; protected procedure DisableSense; procedure EnableSense; function getActiveEdge:TEdgeRegions; function getActiveEdgeOffset(x,y:Integer):TPoint; protected Property ActiveEdge:TEdgeRegions read getActiveEdge; Property SenseEdges:TEdgeSenseEdges read FEdges; Procedure setSenseEdges(const Value:TEdgeSenseEdges);virtual; procedure MouseMove(shiftState:TShiftState;x,y:Integer);override; procedure MouseEnter(shiftState:TShiftState;x,y:Integer);override; procedure MouseExit(shiftState:TShiftState;x,y:Integer);override; procedure Resize;override; protected procedure InitializeObject;Override; end; (* TGridHeaderColumn: This control inherits from TEdgeSenseControl, but adds actual size functionality. All column-header controls can be sized horizontally only. Where the ancestor control adds sensitivity to mouse-hovering over the edges, this control responds to mouse-press while over an edge (start size operation), and will adjust itself according to the user's movements *) TGridHeaderColumn = Class(TEdgeSenseControl) private FSizing: Boolean; FMoving: Boolean; FStartX: Integer; FStartY: Integer; FNowX: Integer; FBaseWidth: Integer; function getGrid:TCustomGrid; protected procedure MouseDown(button:TMouseButton; shiftState:TShiftState;x,y:Integer);override; procedure MouseUp(button:TMouseButton; shiftState:TShiftState;x,y:Integer);override; procedure MouseMove(shiftState:TShiftState;x,y:Integer);override; procedure InitializeObject;Override; public end; IGridHeader = interface procedure ColumnReSizeBegins(const column:TGridHeaderColumn); procedure ColumnReSizeEnds(const column:TGridHeaderColumn); procedure ColumnMoveBegins(Const column:TGridHeaderColumn); procedure ColumnMoveEnds(const column:TGridHeaderColumn); procedure ColumnSized(const column:TGridHeaderColumn); Procedure ColumnMoved(const Column:TGridHeaderColumn); end; TGridHeaderEvent = procedure (Sender:TObject; Column:TGridHeaderColumn); TGridHeaderColumnAddEvent = TGridHeaderEvent; TGridHeaderColumnSizedEvent = procedure (sender:TObject; Column:TGridHeaderColumn; OldSize:TPoint); TGridHeaderColumnMovedEvent = TGridHeaderEvent; TGridHeaderColumnMoveBeginsEvent = TGridHeaderEvent; TGridHeaderColumnMoveEndsEvent = TGridHeaderEvent; TGridHeaderColumnSizeBeginsEvent = TGridHeaderEvent; TGridHeaderColumnSizeEndsEvent = TGridHeaderEvent; (* TGridHeader: This is the container control for TGridHeaderColumn instances. It implements a simple interface for updating during a resize of a column. It also modifies TW3Component->RegisterChild to only allow TGridHeaderColumn instances to register. If you try to create other types of controls with TGridHeader as parent, it will result in an exception *) TGridHeader = Class(TW3CustomControl,IGridHeader) private FOldSize: Tpoint; FItems: Array of TGridHeaderColumn; FOnAdded: TGridHeaderColumnAddEvent; FOnSized: TGridHeaderColumnSizedEvent; FOnMoved: TGridHeaderColumnMovedEvent; FOnMoveBegins:TGridHeaderColumnMoveBeginsEvent; FOnMoveEnds:TGridHeaderColumnMoveEndsEvent; FOnSizeBegins:TGridHeaderColumnSizeBeginsEvent; FOnSizeEnds:TGridHeaderColumnSizeEndsEvent; protected procedure ChildAdded(aChild: TW3Component);override; procedure ChildRemoved(aChild: TW3Component);override; procedure RegisterChild(aChild: TW3Component);override; protected procedure ColumnSized(const column:TGridHeaderColumn); procedure ColumnReSizeBegins(const column:TGridHeaderColumn); procedure ColumnReSizeEnds(const column:TGridHeaderColumn); procedure ColumnMoveBegins(Const column:TGridHeaderColumn); procedure ColumnMoveEnds(const column:TGridHeaderColumn); Procedure ColumnMoved(const Column:TGridHeaderColumn); procedure Resize;Override; Procedure StyleTagObject;override; public property Identifier:Integer; Property Columns[index:Integer]:TGridHeaderColumn read ( FItems[index] ); Property Count:Integer read ( FItems.length ); function Add:TGridHeaderColumn;overload; function Add(Caption:String):TGridHeaderColumn;overload; Procedure Adjust; procedure Clear; procedure UpdateIdentifier; procedure FinalizeObject;Override; published Property OnColumnAdded:TGridHeaderColumnAddEvent read FOnAdded write FOnAdded; Property OnColumnSized:TGridHeaderColumnSizedEvent read FOnSized write FOnSized; Property OnColumnMoved:TGridHeaderColumnMovedEvent read FOnMoved write FOnMoved; Property OnMoveOperationBegins:TGridHeaderColumnMoveBeginsEvent read FOnMoveBegins write FOnMoveBegins; Property OnMoveOperationEnds:TGridHeaderColumnMoveEndsEvent read FOnMoveEnds write FOnMoveEnds; Property OnSizeOperationBegins:TGridHeaderColumnSizeBeginsEvent read FOnSizeBegins write FOnSizeBegins; Property OnSizeOperationEnds:TGridHeaderColumnSizeEndsEvent read FOnSizeEnds write FOnSizeEnds; end; TGridVerticalScrollbar = Class(TW3VerticalScrollbar) end; TGridDataItem = class(TW3CustomControl) public Property ColumnItem:TGridHeaderColumn; end; (* This class represents a single column in a row. It is created as a child of TW3GridDataRow. *) TGridDataColumn = Class(TGridDataItem) protected procedure InitializeObject;Override; end; (* This class represents the left-most edit cursor *) TGridEditorColumn = Class(TGridDataItem) end; IGridDataRow = Interface Procedure Populate; end; TW3GridDataRow = Class(TGridDataItem,IGridDataRow) private FSelected: Boolean; FEditCol: TGridEditorColumn; Procedure Populate; protected function getGrid:TCustomGrid; procedure setSelected(Const Value:Boolean); procedure Resize;Override; procedure InitializeObject;Override; procedure FinalizeObject;Override; protected procedure MouseDown(button:TMouseButton; shiftState:TShiftState;x,y:Integer);override; public Property Parent:TGridRowContainer read (TGridRowContainer(inherited Parent)); Property Index:Integer; Property Identifier:Integer; Property Column[index:Integer]:TGridDataColumn read ( TGridDataColumn(getChildObject(index)) ); Property Count:Integer read ( getChildCount ); Property Selected:Boolean read FSelected write setSelected; procedure UpdateIdentifier; procedure Update; end; TGridRowInfo = Record DOM: TW3GridDataRow; end; (* TGridRowContainer ================= This is a container control which is created as a direct child element on the grid. All rows are created inside this container, and when scrolling occurs - it's actually this container which we target for scrolling *) TGridRowContainer = Class(TW3CustomControl) public Property Parent:TCustomGrid read (TCustomGrid(inherited Parent)); Property Count:Integer read ( inherited getChildCount ); Property Rows[index:Integer]:TW3GridDataRow read ( TW3GridDataRow( inherited getChildObject(index) ) );default; end; TGridOptions = class(TW3OwnedObject) private FScrollDelay: Integer; protected procedure setScrollDelay(Value:Integer);virtual; public Property Owner:TCustomGrid read (TCustomGrid(inherited Owner)); Property AllowSelect:Boolean; property AllowColSize:Boolean; Property AllowColMove:Boolean; Property RowSelect:Boolean; Property ShowEditCol:Boolean; Property ScrollDelay:Integer read FScrollDelay write setScrollDelay; end; IGridStyler = Interface procedure StyleRow(Const Index:Integer; Const Row:TW3GridDataRow); procedure StyleColumn(const Index:Integer; const Col:TGridDataColumn); procedure StyleClientArea(const control:TGridRowContainer); procedure StyleNonClientArea(const control:TCustomGrid); Procedure RowSelected(const Index:Integer; const Row:TW3GridDataRow); procedure RowUnselected(const Row:TW3GridDataRow); procedure ColumnSelected(const index:Integer; const Column:TGridDataColumn); procedure ColumnUnSelected(Column:TGridDataColumn); end; TGridStyler = Class(TObject,IGridStyler) protected procedure StyleRow(Const Index:Integer; Const Row:TW3GridDataRow);virtual; procedure StyleColumn(const Index:Integer; const Col:TGridDataColumn);virtual; procedure StyleClientArea(const control:TGridRowContainer);virtual; procedure StyleNonClientArea(const control:TCustomGrid);virtual; Procedure RowSelected(const Index:Integer; const Row:TW3GridDataRow);virtual; procedure RowUnselected(const Row:TW3GridDataRow);virtual; procedure ColumnSelected(const index:Integer; const Column:TGridDataColumn);virtual; procedure ColumnUnSelected(Column:TGridDataColumn);virtual; end; TGridTools = Class(TW3CustomControl) end; TCustomGrid = Class(TW3CustomControl) const CNT_CREATE_DELAY = 100; private FRowInfo: Array of TGridRowInfo; // LUT for items FExInfo: Array of Integer; // LUT for items with custom height FStack: Array of Integer; FHeader: TGridHeader; FRowSize: Integer = 24; FContainer: TGridRowContainer; FTools: TGridTools; FVScroll: TGridVerticalScrollbar; FOptions: TGridOptions; procedure HandleScroll(Sender:TObject); procedure ProcessStack; protected procedure HandleHeaderColumnMoved (Sender:TObject;Column:TGridHeaderColumn);virtual; procedure HandleHeaderColumnSized(Sender:TObject;Column:TGridHeaderColumn; OldSize:TPoint);virtual; protected function getPageSize:Integer; procedure setGeneralRowHeight(const Value:Integer);virtual; function getContentHeight:Integer; function getTopItemIndex:Integer; function getOffsetForItem(Const RowIndex:Integer):Integer; Procedure Render; protected procedure Resize;override; procedure InitializeObject;override; procedure FinalizeObject;Override; public Property Options:TGridOptions read FOptions; Property RowHeight:Integer read FRowSize write setGeneralRowHeight; Property Header:TGridHeader read FHeader; procedure ShowTools; function IndexOfRow(Const Row:TW3GridDataRow):Integer; function Add(const Index:Integer):TW3GridDataRow; procedure Allocate(Rows:Integer); procedure Clear; end; implementation uses system.memory, system.streams, system.dateutils, system.types.convert; //############################################################################ // TGridStyler //############################################################################ procedure TGridStyler.StyleRow(Const Index:Integer; Const Row:TW3GridDataRow); begin case index mod 2 of 0: Row.background.fromColor(clRed); 1: row.background.fromColor(clWhite); end; end; procedure TGridStyler.StyleColumn(const Index:Integer; const Col:TGridDataColumn); begin end; procedure TGridStyler.StyleClientArea(const control:TGridRowContainer); begin end; procedure TGridStyler.StyleNonClientArea(const control:TCustomGrid); begin end; Procedure TGridStyler.RowSelected(const Index:Integer; const Row:TW3GridDataRow); begin end; procedure TGridStyler.RowUnselected(const Row:TW3GridDataRow); begin end; procedure TGridStyler.ColumnSelected(const index:Integer; const Column:TGridDataColumn); begin end; procedure TGridStyler.ColumnUnSelected(Column:TGridDataColumn); begin end; //############################################################################ // TW3GridDataRow //############################################################################ procedure TGridDataColumn.InitializeObject; begin inherited; self.Font.Size:=12; self.Font.Color:=clWhite; end; //############################################################################ // TW3GridDataRow //############################################################################ procedure TW3GridDataRow.InitializeObject; begin inherited; (Handle)['onmousedown'] := @CBMouseDown; end; procedure TW3GridDataRow.FinalizeObject; begin if FEditCol<>NIL then FEditCol.free; inherited; end; procedure TW3GridDataRow.MouseDown(button:TMouseButton; shiftState:TShiftState;x,y:Integer); begin inherited MouseDown(Button,ShiftState,x,y); if assigned(parent) and assigned(parent.parent) then begin parent.parent.showTools; end; Selected:=not Selected; end; procedure TW3GridDataRow.Update; begin Beginupdate; AddToComponentState([csSized]); EndUpdate; end; procedure TW3GridDataRow.UpdateIdentifier; var x: Integer; begin Identifier:=0; for x:=0 to self.Count-1 do Identifier:=Identifier + ((Column[x].Width + Column[x].left) shl x); end; function TW3GridDataRow.getGrid:TCustomGrid; begin result:=NIL; if (parent<>NIL) and (parent.parent<>NIL) then result:=TCustomGrid(parent.parent); end; procedure TW3GridDataRow.Resize; var x: integer; begin inherited; if FEditCol<>NIL then Begin FEditCol.setBounds(0,0,Height,Height); end; for x:=0 to Count-1 do begin var mItem:=self.Column[x]; if mItem.ColumnItem<>NIL then begin (* mItem.fxScaleTo ( mItem.ColumnItem.Left, 0, mItem.ColumnItem.Width, clientHeight, 0.2 ); *) mItem.SetBounds ( mItem.ColumnItem.Left, 0, mItem.ColumnItem.Width, clientHeight ); end; end; end; Procedure TW3GridDataRow.Populate; var x: Integer; function RandomColor:TColor; begin result:=RGBToColor ( round( Random * 255 ), round( random * 255 ), round( random * 255 ) ); end; begin if Parent<>NIL then begin var mGrid:=self.getGrid; if mGrid<>NIL then begin (* Create edit-cursor column? *) if mGrid.Options<>NIL then Begin if mGrid.Options.ShowEditCol then FEditCol:=TGridEditorColumn.Create(self); end; var rowColor := clNone; if index>=0 then begin if Index mod 2=1 then StyleClass:='RowOdd' else StyleClass:='RowEven'; if index mod 2=1 then RowColor:=RGBToColor(33,33,33) else rowcolor:=RGBToColor(0,0,0); end; (* Now create columns based on header *) for x:=0 to mgrid.header.count-1 do Begin var mItem:=TGridDataColumn.Create(self); mItem.ColumnItem:=mGrid.Header.Columns[x]; if x in [0,2] then mItem.Background.fromColor(RGBToColor(55,55,55)); end; //handle.style['background-color']:=ColorToWebStr(rowColor); background.FromColor(rowColor); (* Update identifier, used by the grid to know if a row needs a resize or have a different layout from the main-form *) UpdateIdentifier; end; end; end; procedure TW3GridDataRow.setSelected(Const Value:Boolean); begin if value<>FSelected then begin FSelected:=Value; case Value of True: CSSClasses.Add("RowSelected"); false: CSSClasses.RemoveByName("RowSelected"); end; end; end; //############################################################################ // TGridOptions //############################################################################ procedure TGridOptions.setScrollDelay(Value:Integer); begin value:=TInteger.EnsureRange(Value,0,100); if value<>FScrollDelay then begin FScrollDelay:=Value; end; end; //############################################################################ // TCustomGrid //############################################################################ procedure TCustomGrid.InitializeObject; begin inherited; FOptions:=TGridOptions.Create(self); FHeader:=TGridHeader.Create(self); FVScroll:=TGridVerticalScrollbar.Create(self); FVScroll.OnChanged:=HandleScroll; FVScroll.Enabled:=False; FVScroll.width:=24; FVScroll.Background.FromColor(clWhite); FContainer:=TGridRowContainer.Create(self); FContainer.Background.FromColor(clGreen); FHeader.OnColumnMoved:=HandleHeaderColumnMoved; FHeader.OnColumnSized:=HandleHeaderColumnSized; end; procedure TCustomGrid.FinalizeObject; begin FContainer.free; FVScroll.free; FHeader.clear; FHeader.free; FOptions.free; inherited; end; procedure TCustomGrid.HandleHeaderColumnSized (Sender:TObject;Column:TGridHeaderColumn;OldSize:TPoint); begin Header.UpdateIdentifier; Render; end; procedure TCustomGrid.HandleHeaderColumnMoved (Sender:TObject;Column:TGridHeaderColumn); var x: Integer; begin var mIndex:=getTopItemIndex; var mPage:=self.getPageSize; Header.UpdateIdentifier; //Render; for x:=0 to mPage-1 do begin var mRow:=FRowInfo[mIndex + x].DOM; if (mRow<>NIL) then begin mRow.BeginUpdate; mRow.addToComponentState([csSized]); mRow.EndUpdate; end; end; end; procedure TCustomGrid.setGeneralRowHeight(const Value:Integer); begin FRowSize:=TInteger.EnsureRange(Value,12,128); end; procedure TCustomGrid.Clear; var x: Integer; begin try for x:=0 to self.FRowInfo.length-1 do begin if FRowInfo[x].DOM<>NIL then FRowInfo[x].DOM.free; end; finally FRowInfo.Clear; end; end; function TCustomGrid.getTopItemIndex:Integer; begin result:=FVScroll.Position; end; function TCustomGrid.getOffsetForItem(Const RowIndex:Integer):Integer; begin result:=RowIndex * FRowSize; end; function TCustomGrid.getPageSize:Integer; Begin result:=FContainer.ClientHeight div FRowSize; end; function TCustomGrid.getContentHeight:Integer; var x: Integer; begin result:=(FRowInfo.Length-FExInfo.length) * FRowSize; for x:=0 to FExInfo.length-1 do inc(result,FRowInfo[x].DOM.height); end; procedure TCustomGrid.ProcessStack; var mItem: TW3GridDataRow; mIndex: Integer; Begin if FStack.Count>0 then begin (* We leave the number on the stack, that way we dont end up creating duplicates, which would drain memory line insane *) mIndex:=FStack.Peek; try if FRowInfo[mIndex].DOM=NIL then Begin mItem:=Add(mIndex); FRowInfo[mIndex].DOM:=mItem; mItem.Column[0].InnerHTML:=IntToStr(mIndex); mItem.SetBounds ( 0, getOffsetForItem(mIndex), clientwidth - self.FVScroll.width, FRowSize ); end; finally FStack.Pop; end; if FStack.count>0 then ProcessStack; end; end; Procedure TCustomGrid.Render; var x: Integer; mTopIndex: Integer; mPageItems: Integer; mObj:TW3GridDataRow; begin mTopIndex:=getTopItemIndex; mPageItems:=FContainer.Height div FRowSize; for x:=0 to mPageItems-1 do begin var mIndex:= x + mTopIndex; (* Get allocated row object *) mObj:=FRowInfo[mIndex].DOM; (* No allocated row? Push to stack, late create *) if mObj=NIL then begin if FStack.IndexOf(mIndex)=-1 then FStack.Push(mIndex); end else begin (* Is the row identifier different from the header layout? *) if mObj.Identifier<>FHeader.Identifier then begin mObj.UpdateIdentifier; mObj.update; end; end; end; if FStack.Length>0 then ProcessStack; end; procedure TCustomGrid.Allocate(Rows:Integer); begin if FRowInfo.length>0 then Clear; if Rows>0 then begin FRowInfo.Clear; FRowInfo.SetLength(Rows); writeln("Allocating " + rows.tostring + " rows"); FVScroll.Enabled:=true; FVScroll.Total:=Rows; FVScroll.Position:=0; FVScroll.PageSize:=getPageSize; FVScroll.Visible:=true; w3_requestAnimationFrame(Render); end; end; function TCustomGrid.IndexOfRow(Const Row:TW3GridDataRow):Integer; var x: Integer; begin result:=-1; for x:=0 to FRowInfo.Length do begin if FRowInfo[x].DOM = Row then Begin result:=x; break; end; end; end; procedure TCustomGrid.ShowTools; var mHeight: integer; mTargetPos: integer; mTestPos: Integer; begin if not (csDestroying in ComponentState) then begin if FRowInfo.Length>0 then begin mTargetPos:=(ClientHeight - FHeader.Height) div 2; mTestPos :=FHeader.top + FHeader.height + 10; mHeight:=(ClientHeight-FHeader.height) div 2; if FContainer.top>=mTestPos then FContainer.fxScaleTo(0,FHeader.height,FContainer.Width,clientHeight-Fheader.Height,0.3, procedure () begin FVScroll.PageSize:=getPageSize; end) else FContainer.fxScaleTo(0,mHeight,FContainer.width,FContainer.height ,0.4, procedure () begin FContainer.height := (ClientHeight -mHeight) - 20; FVScroll.PageSize:=getPageSize; end); end; end; end; function TCustomGrid.Add(const Index:Integer):TW3GridDataRow; Begin result:=TW3GridDataRow.Create(FContainer); result.setBounds( 0, Header.BoundsRect.bottom + getContentHeight, FContainer.clientwidth, FRowSize); result.Index:=Index; result.BeginUpdate; try (result as IGridDataRow).populate; except on e: exception do begin result.free; result:=NIL; exit; end; end; result.AddToComponentState([csSized]); result.EndUpdate; end; procedure TCustomGrid.HandleScroll(Sender:TObject); begin if (csReady in ComponentState) then Begin FContainer.ScrollInfo.ScrollTo(0,FVScroll.Position * FRowSize); if FOptions.ScrollDelay=0 then Render else w3_setTimeout(Render,FOptions.ScrollDelay); end; end; procedure TCustomGrid.Resize; var mScaledHeight: Integer; begin inherited; If Handle.Valid and handle.ready then begin FHeader.SetBounds(0,0,clientwidth,32); FVScroll.setBounds( clientwidth-FVScroll.width, FHeader.height, FVScroll.width, clientheight - Fheader.height); //mScaledHeight:=mScaledHeight * FRowSize; //end else mScaledHeight:=clientHeight - (FHeader.Height); FContainer.setBounds(0,FHeader.height, clientWidth-FVScroll.width, mScaledHeight); end; end; //############################################################################ // TGridHeader //############################################################################ procedure TGridHeader.FinalizeObject; begin inherited; end; Procedure TGridHeader.styleTagObject; Begin inherited; end; procedure TGridHeader.Resize; var wd: Integer; x: Integer; dx: Integer; cnt: Integer; begin cnt:=Count; if cnt>0 then begin if handle.valid and handle.ready then begin dx:=0; wd:=clientWidth div cnt; for x:=0 to cnt-1 do begin wd:=Columns[x].width; Columns[x].setBounds(dx,0,wd,clientHeight); inc(dx,wd); end; end; end; end; procedure TGridHeader.RegisterChild(aChild: TW3Component); begin if (aChild<>NIL) then begin if (aChild is TGridHeaderColumn) then inherited RegisterChild(aChild) else Raise Exception.Create ('Only column controls can be added to a grid header'); end; end; procedure TGridHeader.ChildAdded(aChild: TW3Component); begin inherited ChildAdded(aChild); FItems.Add(TGridHeaderColumn(aChild)); if not (csDestroying in ComponentState) and not (csLoading in ComponentState) then Resize; end; procedure TGridHeader.ChildRemoved(aChild: TW3Component); var mIndex: Integer; begin inherited ChildRemoved(aChild); mIndex:=FItems.IndexOf(TGridHeaderColumn(aChild)); if mIndex>=0 then FItems.delete(mIndex,1); if not (csDestroying in ComponentState) and not (csLoading in ComponentState) then Resize; end; procedure TGridHeader.ColumnReSizeBegins(const column:TGridHeaderColumn); begin FOldSize:=TPoint.Create(Column.Width,Column.Height); if assigned(FOnSizeBegins) then FOnSizeBegins(self,Column); end; procedure TGridHeader.ColumnReSizeEnds(const column:TGridHeaderColumn); begin if assigned(FOnSized) then FOnSized(self,Column, FOldSize); if assigned(FOnSizeEnds) then FOnSizeEnds(self,Column); end; procedure TGridHeader.ColumnMoveBegins(Const column:TGridHeaderColumn); begin if assigned(FOnMoveBegins) then FOnMoveBegins(self,Column); end; procedure TGridHeader.ColumnMoveEnds(const column:TGridHeaderColumn); begin if assigned(FOnMoveEnds) then FOnMoveEnds(self,Column); end; Procedure TGridHeader.ColumnMoved(const Column:TGridHeaderColumn); function GetChildrenSortedByXPos: Array of TGridHeaderColumn; var mCount: Integer; x: Integer; mAltered: Boolean; mObj: TGridHeaderColumn; mLast: TGridHeaderColumn; mCurrent: TGridHeaderColumn; begin mCount := GetChildCount; if mCount>0 then begin (* populate list *) for x := 0 to mCount - 1 do begin mObj := Columns[x]; if (mObj is TGridHeaderColumn) then Result.add(mObj); end; (* sort by X-pos *) if Result.Count>1 then begin repeat mAltered := False; for x := 1 to mCount - 1 do begin mLast := TGridHeaderColumn(Result[x - 1]); mCurrent := TGridHeaderColumn(Result[x]); if mCurrent.left + (mCurrent.width div 2) < mLast.left + (mLast.width div 2) then begin Result.Swap(x - 1,x); mAltered := True; end; end; until mAltered=False; end; end; end; begin var mItems := GetChildrenSortedByXPos; var x:=0; var dx:=0; var mWaits:=mItems.count; for x:=0 to mItems.count-1 do begin mItems[x].fxMoveTo(dx,mItems[x].Top,0.3, procedure () begin dec(mWaits); if mWaits=0 then begin if assigned(FOnMoved) then FOnMoved(self,Column); end; end); inc(dx,mItems[x].width); end; FItems:=mItems; end; procedure TGridHeader.ColumnSized(const column:TGridHeaderColumn); begin w3_requestAnimationFrame(Resize); end; Procedure TGridHeader.Adjust; begin Resize; end; procedure TGridHeader.Clear; procedure doClear; var x: Integer; mItem: TGridHeaderColumn; begin for x:=0 to getChildCount-1 do Begin if (getChildObject(x) is TGridHeaderColumn) then Begin mItem:=TGridHeaderColumn(getChildObject(x)); mItem.free; end; end; end; begin if not (csDestroying in ComponentState) then begin BeginUpdate; try doClear; finally addToComponentState([csSized]); endUpdate; UpdateIdentifier; end; end else doClear; end; procedure TGridHeader.UpdateIdentifier; var x: Integer; begin (* Calculate new identifier *) Identifier:=0; for x:=0 to FItems.Length-1 do Identifier:=Identifier + ((Columns[x].Width + Columns[x].left) shl x); end; function TGridHeader.Add:TGridHeaderColumn; begin beginupdate; result:=TGridHeaderColumn.Create(self); result.width:=100; result.height:=clientHeight; addToComponentState([csSized]); endupdate; UpdateIdentifier; w3_setTimeOut( procedure () begin if assigned(FOnAdded) then FOnAdded(self,result); end, 10); end; function TGridHeader.Add(Caption:String):TGridHeaderColumn; begin beginupdate; result:=TGridHeaderColumn.Create(self); result.width:=100; result.height:=clientHeight; result.caption:=Caption; addToComponentState([csSized]); endupdate; UpdateIdentifier; w3_setTimeOut( procedure () begin if assigned(FOnAdded) then FOnAdded(self,result); end, 10); end; //############################################################################ // TGridHeaderColumn //############################################################################ procedure TGridHeaderColumn.InitializeObject; begin inherited; (* Manually initialize event handlers *) (Handle)['onmousedown'] := @CBMouseDown; (Handle)['onmouseup'] := @CBMouseUp; setSenseEdges([scRight]); Caption:='Column'; font.Name:="verdana"; font.size:=12; font.color:=clWhite; end; procedure TGridHeaderColumn.MouseDown(button:TMouseButton; shiftState:TShiftState;x,y:Integer); begin inherited MouseDown(button,shiftstate,x,y); if (ActiveEdge in [scLeft,scRight]) then begin DisableSense; setCapture; FSizing:=True; FStartX:=x; FBaseWidth:=Width; (Parent as IGridHeader).ColumnReSizeBegins(self); end else Begin if clientRect.ContainsPos(x,y) then Begin FMoving:=True; DisableSense; setCapture; FStartX:=x; FStartY:=y; w3_setStyle(Handle,'cursor','move'); (Parent as IGridHeader).ColumnMoveBegins(self); end; end; end; procedure TGridHeaderColumn.MouseMove(shiftState:TShiftState;x,y:Integer); var dx: Integer; begin inherited MouseMove(ShiftState,x,y); if FSizing then begin FNowX:=x; dx:=FNowX - FStartX ; SetWidth(FBaseWidth + dx); (TGridHeader(parent) as IGridHeader).ColumnSized(self); end; if FMoving then begin left:=self.ClientToScreen( TPoint.Create(x - FStartX,top) ).x; end; end; function TGridHeaderColumn.getGrid:TCustomGrid; begin result:=NIL; if Parent<>NIL then Begin if Parent.parent<>NIL then result:=TCustomGrid(parent.Parent); end; end; procedure TGridHeaderColumn.MouseUp(button:TMouseButton; shiftState:TShiftState;x,y:Integer); begin inherited MouseUp(button,shiftstate,x,y); if FSizing then Begin FSizing:=False; ReleaseCapture; EnableSense; (Parent as IGridHeader).ColumnSized(self); (Parent as IGridHeader).ColumnReSizeEnds(self); end; if FMoving then begin FMoving:=false; ReleaseCapture; EnableSense; w3_setStyle(Handle,'cursor','default'); (Parent as IGridHeader).ColumnMoved(self); end; end; //############################################################################ // TEdgeSenseControl //############################################################################ Procedure TEdgeSenseControl.initializeObject; Begin inherited; FEdges:=[scLeft,scTop,scRight,scBottom]; (* Manually hook up events *) (Handle)['onmousemove'] := @CBMouseMove; (Handle)['onmouseover'] := @CBMouseEnter; (Handle)['onmouseout'] := @CBMouseExit; FSense:=True; AlignText:=TTextAlign.taCenter; end; Procedure TEdgeSenseControl.setSenseEdges(const Value:TEdgeSenseEdges); begin if (scLeft in Value) then Include(FEdges,scLeft) else Exclude(FEdges,scLeft); if (scTop in Value) then include(FEdges,scTop) else Exclude(FEdges,scTop); if (scRight in Value) then Include(FEdges,scRight) else Exclude(FEdges,scRight); if (scBottom in Value) then include(FEdges,scBottom) else Exclude(FEdges,scBottom); end; procedure TEdgeSenseControl.Resize; begin inherited; FRects[0]:=TRect.CreateSized(0,CNT_SIZE,CNT_SIZE,ClientHeight-CNT_SIZE); FRects[1]:=TRect.CreateSized(0,0,ClientWidth,CNT_SIZE); FRects[2]:=TRect.CreateSized(ClientWidth-CNT_SIZE,CNT_SIZE,Clientwidth,ClientHeight-CNT_SIZE); FRects[3]:=TRect.CreateSized(0,ClientHeight-CNT_SIZE,ClientWidth,CNT_SIZE); end; procedure TEdgeSenseControl.CheckCorners(const x,y:Integer); var mItem:TRect; mIndex: Integer; begin FEdgeId:=-1; for mItem in FRects do begin if mItem.ContainsPos(x,y) then begin FEdgeId:=mIndex; break; end; inc(mIndex); end; end; function TEdgeSenseControl.getActiveEdgeOffset(x,y:Integer):TPoint; begin if FEdgeId>=0 then result:=TPoint.Create ( x - FRects[FEdgeId].Left, y - FRects[FEdgeId].Top ); end; function TEdgeSenseControl.getActiveEdge:TEdgeRegions; begin if (FEdgeId>=0) and (FEdgeId<=3) then result:=TEdgeRegions(FEdgeid) else result:=scNone; end; procedure TEdgeSenseControl.UpdateCursor; const CN_LEFT = 0; CN_TOP = 1; CN_RIGHT = 2; CN_BOTTOM = 3; var mCursor: string; procedure doDefault; begin if mCursor<>'default' then w3_setStyle(Handle,'cursor','default'); end; begin mCursor:=w3_getStyleAsStr(Handle,'cursor').LowerCase; case FEdgeId of CN_LEFT: Begin if (scLeft in FEdges) then begin if mCursor<>'col-resize' then w3_setStyle(Handle,'cursor','col-resize'); end else doDefault; end; CN_RIGHT: Begin if (scRight in FEdges) then begin if mCursor<>'col-resize' then w3_setStyle(Handle,'cursor','col-resize'); end else doDefault; end; CN_TOP: begin if (scTop in FEdges) then begin if mCursor<>'row-resize' then w3_setStyle(Handle,'cursor','row-resize'); end else doDefault; end; CN_BOTTOM: begin if (scBottom in FEdges) then begin if mCursor<>'row-resize' then w3_setStyle(Handle,'cursor','row-resize'); end else doDefault; end; else doDefault; end; end; procedure TEdgeSenseControl.DisableSense; begin FSense:=False; end; procedure TEdgeSenseControl.EnableSense; begin FSense:=True; end; procedure TEdgeSenseControl.MouseMove(shiftState:TShiftState;x,y:Integer); begin if FSense then begin CheckCorners(x,y); UpdateCursor; end; inherited MouseMove(ShiftState,x,y); end; procedure TEdgeSenseControl.MouseEnter(shiftState:TShiftState;x,y:Integer); begin if FSense then begin CheckCorners(x,y); UpdateCursor; end; inherited MouseEnter(ShiftState,x,y); end; procedure TEdgeSenseControl.MouseExit(shiftState:TShiftState;x,y:Integer); begin if FSense then Begin CheckCorners(x,y); UpdateCursor; end; inherited MouseExit(ShiftState,x,y); end; end.
Notes on the grid
The reason the grid is able to deal with hundreds of rows quickly, is because i use a stack technique. A row is only allocated once it steps into view, or is assigned data.
You are more than welcome to play around with it. Just keep in mind that its experimental at this stage, and the final version will no doubt be more polished.
Well, hope you got inspired! Enjoy, and merry xmas!
Angular + knockout + bootstrap = Smart Mobile
I have been suuper busy lately with work so I havent been able to upload the latest “angular, knockout, bootstrap KILLER” library.
In essence it goes like this:
- Bootstrap is good for UI’s
- Angular i good for data binding
- Knockout is the same, but with a few cool twists
Smart mobile studio absorbs them all through object orientation, which in turn renders them useless compared to our model 🙂
Stay tuned for more eh.. knockout code 😀
Swirly graphics
Just did a small 10 minute experiment in Smart Pascal. Essentially what I want is to be able to pre-calculate a nice soft path between X number of points. Which is essentially what is known as a bezier curve. This can be used in many situations from plotting pixels, moving sprites or indeed – animating elements with CSS.
Here is my 10 minute experiment:
function bezier(p1,p2,p3,p4:TPoint;t:float):TPoint; begin var mum1 := 1- t; var mum13 := mum1 * mum1 * mum1; var mu3 := t * t * t; result:=TPoint.Create( round(mum13 * p1.x + 3*t * mum1*mum1 * p2.x+3*t*t * mum1*p3.x + mu3*p4.x), round(mum13 * p1.y + 3*t * mum1*mum1 * p2.y+3*t*t * mum1*p3.y + mu3*p4.y) ); end; procedure TForm1.W3Button1Click(Sender: TObject); var mBitmap: TBitmap; x: Integer; p1,p2,p3,p4: TPoint; begin mBitmap:=TBitmap.create; try mBitmap.Allocate(400,400); p1:=TPoint.Create(10,60); p2:=Tpoint.create(100,40); p3:=Tpoint.create(180,60); p4:=Tpoint.create(200,180); mBitmap.canvas.pen.color:=clRed; mBitmap.canvas.MoveTo(p1); mBitmap.canvas.lineTo(p2); mBitmap.canvas.lineto(p3); mBitmap.Canvas.lineto(p4); var f :=0.0; for x:=0 to 100 do begin f:=x / 100; var mpt := bezier(p1,p2,p3,p4,f); writeln(mpt.toString()); mBitmap.canvas.Pixels[mpt.x,mpt.y]:=clBlue; end; w3panel1.Background.FromURL(mBitmap.Canvas.ToDataURL('png')); finally mBitmap.free; end; end;
Lo and behold the mighty swirley path!
You must be logged in to post a comment.