Easy pinch zoom for ImageView

As part of my Meetup for BlackBerry 10 app, I provide the photo albums for the user’s groups.  With the list of albums, the user can drill down to see a list of the images and then click on the individual image. The image is loaded into the ImageView component from a url on Meetup.com.  Due the obvious screen size difference between the files upload and a phone, the image is re-sized to fit on the available screen.  Regardless if the screen is the 1:1 Q5/10 or the 16:8/9 Z10/30, the user will need to zoom in on the picture and the scroll the image to see the areas larger than the screen.

When I was searching for a solution for this, I found examples using the gesture handlers to catch the pinch action and do something based on that action.  While that seemed to work for making the image larger, however, it wasn’t working well for making the image smaller.  The method for making the image larger was to increase the ScaleX and ScaleY values.  Also, I wasn’t able to move the image around to see the other parts of the zoomed image.  The gesture handler was attached to the ImageView component which was inside a ScrollView component.  I started focusing on the ScrollView and happened upon the basic settings that would make the pinchToZoom functionality of the ScrollView control the child ImageView.

ScrollView {
	id: gestureContainer
	scrollViewProperties {
		scrollMode: ScrollMode.Both
	}
	scrollRole: ScrollRole.Main
	scrollViewProperties.minContentScale: 1.0
	scrollViewProperties.maxContentScale: 8.0
	scrollViewProperties.pinchToZoomEnabled: true
	scrollViewProperties.overScrollEffectMode: OverScrollEffectMode.Default

	WebImageView {
		id: singleImg
		url: ListItemData.photo_link
	}
}

The key is to set the ScrollRole to Main, enable the pinchToZoom and set the overScrollEffectMode to Default. With all of these together, the image can be pinch to zoomed in and out.  And the image can be moved around without it snapping back to the middle when the touch event ends.  I set the limit of the scale to 8 times normal size, because it is pretty much pointless above that due to the resolution of the images loaded.

The one issue was, that the last zoom level carried over to any other image selected.  What I needed it to do is reset itself back to the initial zoom.  After attempting to reset the scale values, I found that you have to reset the viewableArea.


onSelectPhotoURLChanged: {
	gestureContainer.resetViewableArea();
	singleImg.url = selectPhotoURL;
	console.debug("on selectPhotoURL: " + selectPhotoURL);
}

Since I check for the image URL change, I put the resetViewableArea before it loads the image. This does allow for the same image to be viewed at the previous zoom level, which isn’t a problem.  I think I would have made myself crazy trying to get the gesture handler to work both ways.  Too bad the documentation doesn’t have this simple solution.

Better way of adding a Sheet

ssSheetI am making some more updates to my Meetup for BlackBerry 10 app and wanted to add a page for Settings, which is accessible from the pull down app menu.   Screens in Cascades are either a Page or Sheet contained by themselves or in a Tab or Navigation panes.  My application is already setup using a TabbedPane, so in order to add something that isn’t a Tab, I need to display a Sheet.  Previously, I added a sheet to a photo album page to show a single photo.  After you define the sheet on the Page, to display it, you simple call “open()”.  The issues is that the Sheet definition is part of the attached objects for the Page.  And within that Sheet definition is a Page in order to “close” the sheet.


 attachedObjects: [
	Sheet {
		id: photoSheet
		content: 
		Page {
			WebImageView {
				id:singleImg
				url: ListItemData.photo_link
				horizontalAlignment: HorizontalAlignment.Center
				verticalAlignment: VerticalAlignment.Center
				// visible: (img.loading == 1.0)
				scalingMethod: ScalingMethod.AspectFit
				//preferredWidth: Qt.mainTab.width
				//preferredHeight: Qt.mainTab.height
			}
			onCreationCompleted: { 
				singleImg.url = selectPhotoURL; 
				console.debug("selectPhotoURL: " + selectPhotoURL);
			}
			actions: [
				ActionItem {
					id: actionClose
					title: "Close"
					imageSource: "asset:///images/previous.png"
					ActionBar.placement: ActionBarPlacement.OnBar
					onTriggered: {
						photoSheet.close()
					}
				}
			]
		}
		
	}
]

While this is fine for a simple Sheet, if you want your sheet do to more, the code gets a little crowded on the initial page.  Also, if you wanted to call that Sheet elsewhere in the app, you would have to rewrite the code.

The fix is fairly simple, but it just took me a few times to wrap my head around it.  Previously when I did a component definition, I added the Page or Content like you would call a function.  Well, you can do a Sheet in the same method.  However,  you still need the Page action available, so instead of creating a Sheet and then adding it to the definition, I created a Container (SettingsContainer) for all the things the sheet needed to handle and called the Container inside of the Sheet definition.


attachedObjects: [
	Sheet {
		id: settingsSheet
		Page {
			id: settingsPage
			objectName: "settingsPage"
			SettingsContainer {}
			actions: [
				ActionItem {
					id: actionClose
					title: "Close"
					imageSource: "asset:///images/previous.png"
					ActionBar.placement: ActionBarPlacement.OnBar
					onTriggered: {
						settingsSheet.close()
					}
				}
			]
		}
	}
]

 

I did try to create a component definition and add a Sheet as the content, but it only allows for a Page.  Doing it this way does allow for the core of the sheet to be available in other areas and the actions to open it and close it left on the calling Page.

A look at my download reports

I released my first BB10 app back on Oct. 28th (trying to get a free Red Z10) and then I did an update on Dec. 04. When you do a basic report in the BlackBerry Vendor World, you see something like this.

MeetupForBlackBerry10-09DEC

Which doesn’t tell you much, except for that a bunch of people updated the app when I released the new version.

SpreadSheet_meetup

To get more detailed information, you do scheduled report (which is a misleading title).  A scheduled report is an CSV output of the application, date downloaded, batch file downloaded, application version, device model, device OS, country, carrier and locale.  This is a little more helpful, because you can see where people are downloading from and possibly get an idea of how many people are upgrading to the new version.

Some of the things I found interesting was there were more than one downloads from RIM as a Carrier.

I can see Q5, Q10 and Z30 phone types, but it seems that the Z10s show up as Qualcomm GPU or Imagination GPU.

I had a lot of downloads in Africa. I didn’t expect it to be popular in that area of the world. Including the small island country of Mauritius. I did a check and was surprised to find that they do actually have Meetup groups there.

4 people downloaded the 1.0 app after the 1.1 app was available.  All 4 in different countries, with different carriers.

MeetupForBlackBerry10-09DEC_marks

Disappointingly, it looks like I only had about 221 updates. Which means the 771 initial downloads weren’t all kept or haven’t been updated. I plan on doing some more updates and then submitting it to Built for BlackBerry. I am hoping that at that point, those who initially downloaded, but uninstalled it, will come back to the app. Anyways, wanted to share my thoughts.

My first BlackBerry 10 app

Meetup for BlackBerry10

I just updated my first BlackBerry 10 application; Meetup for BlackBerry 10.

I was attempting to get the app released by a deadline in order to win a free Z10 in Red.  The offer was to be one of the first 500 developers to build an app and then get it certified as Built for BlackBerry in a certain time frame.   Well, I didn’t get it submitted in time, because there is a big delay between getting your app submitted and getting it reviewed; then apparently there is a delay for getting Built for BlackBerry certified.  Because I rushed things, it didn’t work that great in version 1.0, which resulted in a couple of bad reviews.  My main problem was finding the time to work on the application, which is hard when you have a less than year old son and a pregnant wife.  Then life threw me another curve and I lost my job.  So, I have been working on fixing a couple of issues with the 1.0 release and adding new features.

One of the bugs 1.o had was that the UTC offset for the events was not correct.  I tried just adding the long value to the date to get the correct date and time, but that didn’t work.  The solution was simple and I guess if I had more time to look at things I would have noticed it the first time.  The fix is to use setUtcOffset .  D’uh.


QDateTime tmpEventStartDT = QDateTime::currentDateTime();
tmpEventStartDT.setMSecsSinceEpoch(query.value("eventDate").toLongLong());
tmpEventStartDT.setUtcOffset(utcOffsetSeconds);

Another issue happened to due with how Meetup did repeating events.  Instead of using an Integer for each new event, repeating events have an alphanumeric value.  Because I was looking for an Integer, none of the repeating events were showing up after the first event.  I didn’t even notice this in version 1.0 and I finally noticed it after I made some clean up to the events list and noticed that not all the events were showing up.  It took a bit of effort to get that debug cleared.

The major flaw of the 1.0 version was that it didn’t show the Event or Group detail.  The reason I left it as a link was the lack of time in figuring out a way to show the event detail easily.  I haven’t messed with Sheets yet, but I ended up using Tabs to display the Event information, plus the RSVP and Photos list.  I did use a Sheet to show the final individual photo for an photo album.  But, with that there are still limitations.  I need to figure out how to zoom in on a photo.

The Information pages, photo albums and RSVP lists are all new features for 1.1, but they still need to be tweaked.  But, just as a fussy baby is rushing me on this post, hopefully I will get around to making more tweaks to what is available and add even more features.

 

UPDATE :

I got the approval email for my update.  So, it took about 2 days to approve the changes.  Now I modified the description and screen shots, because I didn’t think I should do it before the change was updated.  Hopefully it takes less time to modify those things.  I didn’t realize that it wasn’t automatically updating things, so I mistakenly created 3 updates in an attempt to get the text and screen shot changes to save.  Oh well, lessons learned.

Privacy Policy, Change Log and the App Menu Definition

In an attempt to get a free Red Z10, I tried to get my Meetup application finished by a drop dead date and submitted to Built for BlackBerry by Oct. 21st.  That didn’t work, but the app is available, in probably more of a beta state.  I’m working on the fixes when I can.  As part of the Built for BlackBerry requirements, you have to provide a link to a Privacy Policy from your application.  From the developer forums, there were two ways to link to the Privacy Policy.  One method seemed to be a link from the vendor portal, which adds the link to your application download page.  The other option was to link it some where from inside the application itself.  Since the instructions on Built for BlackBerry seem to point to the latter option, I tried to figure out the best way to handle adding the link.

The first thing I decided was that I wasn’t going to keep the Privacy Policy within the application.  Just in case I needed it available for the application download page, I figured that it would be better to host the policy on-line (I used a generic privacy policy example found in the developer forums) and link it from where ever I need to link it from. Also, the privacy policy for one application will most likely be different than another, so I had to make sure I could have multiple policies available.

To help things out, I found a simple c++ code for launching the browser, which I put in the the ApplicationUI class for use for all simple HTML browser launches.


void ApplicationUI::launchBrowser(QString url)
{
	navigator_invoke(url.toStdString().c_str(), 0);
}

From there, I simply call the ApplicationUI class and the launchBrowser function in something simple like an onTrigger event.


onTriggered: {
	_meetupApp.launchBrowser("http://deanlogic.com/PrivacyPolicy/BB10Meetup.html");
}

The next step was to add it somewhere on the application that the user could easily access it.  At first I thought about putting it on a start page or sheet, but decided it would be best to link it from the application menu, which is the pull down menu that is available anywhere in the application. While it might not be the ideal place, it made sense for me.  The Menu Definition has two default menu types; Info/Help and Settings.  With those options, you get default icons and positions.

The   Menu Definition - help Help displays on the left and the Menu Definition - settings Settings option displays on the right.

This is a great little shortcut for adding Help and Settings information.  But, I needed something a little different and luckily there is an option for that as well call  an Action Item, which you can have 3 of, if you have the Help and Settings actions or 5, if you don’t have those set.  And, like when you use them in other menu positions, you can add your own image source for the icon. These menu items show up between the Settings and Help options, no matter how you add them.  For adding the Privacy Policy and a Change Log item, it requires adding an “action” area and then each action item.


Menu.definition: MenuDefinition {
	helpAction: HelpActionItem {
		id: appHelp
		title: qsTr("Help") + Retranslate.onLocaleOrLanguageChanged
		onTriggered: {
			// some Help screen action here
		}
	}
	settingsAction: SettingsActionItem {
		id: appSettings
		title: qsTr("Settings") + Retranslate.onLocaleOrLanguageChanged
		onTriggered: {
			// some Settings screen action here
		}
	}
	actions: [
		ActionItem {
			id: changeLog
			title: qsTr("Change Log") + Retranslate.onLocaleOrLanguageChanged
			imageSource: "images/ChangeLog.png"
			onTriggered: {
				_meetupApp.launchBrowser("http://deanlogic.com/ChangeLog/BB10Meetup.html");
			}
		},
		ActionItem {
			id: privacyPolicy
			title: qsTr("Privacy Policy") + Retranslate.onLocaleOrLanguageChanged
			imageSource: "images/PrivacyPolicy.png"
			onTriggered: {
				_meetupApp.launchBrowser("http://deanlogic.com/PrivacyPolicy/BB10Meetup.html");
			}
		}
	]
}

I placed this on the main.qml page and it will show up everywhere on the application by simply pulling down from the top bezel. Without the Help and Settings menu items, each action item lines up on the outer sides of the menu.

MenuDefinition-allMenuDefinition-actionOnly

Checking for a COLUMN before ALTER TABLE

SQLite is one of the many ways to store data on BB10. I find it is easier for me to use sql calls to get the information I want then doing the old BB OS way of using Vectors.  Also, instead of using DataModels to populate a form in BB10, I am calling from the database and then creating a instance of the item to fill in, once I have retrieved the data (I’ll write up another post about that).  The issue with SQLite is that it is “Lite”, which means not all functionality is available in order to reduce the size.  For example, it would be nice to check to see if a COLUMN existed before doing an ALTER TABLE so that an error won’t happen.

For example


IF DOES NOT EXIST ALTER TABLE myTable ADD COLUMN myNewColumn VARCHAR

While you could just let this error out, that’s not a good way to check.  And for a mobile application, you don’t want this error happening every time the application starts.  The work around is to use a PRAGMA statement to check the table for the column before doing the ALTER.   It seems like a bunch more code, but it is probably the best way to prevent just handling an error.  Also, if you have multiple changes to the table based on if a particular column does not exist, then you could add other actions in the ALTER section.


// Check to see if row exists
const QString colCheck = "PRAGMA table_info('myTable')";
bool alterTable = true;
if(query.exec(colCheck)) {
  while(query.next()) {
    // the second value ( 1 ) is the column name
    if(query.value(1).toString() == "myNewColumn") {
      alterTable = false;
    }
  }
} else {
  const QSqlError error = query.lastError();
  qDebug() << tr("ALTER  TABLE ERROR:  %1").arg(error.text());
}
// Alter table if column does not exist
const QString colAdd = "ALTER TABLE ADD COLUMN myNewColumn VARCHAR";
if(alterTable) {
  if(query.exec(colStateAdd)) {
    qDebug() << "COLUMN myNewColumn ADDED";
  } else {
    const QSqlError error = query.lastError();
    qDebug() << tr("ADD COLUMN myNewColumn TABLE ERROR:  %1").arg(error.text());
  }
}

QML function from child Tab c++ call

Well, that’s a mouth full of a title.  It means, that I am going to share how I called a JavaScript function in my main QML page from a Tab that called a c++ function.  Okay…still need to break it down even further.

I am creating an application that uses OAuth 2.0 to get and send data.  If the application hasn’t already stored the access tokens, I need for the user to give the application access.  When the authentication is complete, I need to remove the Login Tab and make the other Tabs enabled.  Since the authentication function has to wait for a return and then it handles that return in a separate function, I can’t simply point a button on the QML to the c++ function and wait for a response.  What needs to happen is that a function on the main QML page is given “signal” to start when the return function is complete.

Simple Flow: press authentication button -> get form -> submit form -> get access token -> send access token -> get authentication token -> signal main function -> close login tab

The key here is using a “signal” to kick of the QML function.  But, the trick is that QML’s don’t have signals.  What you have to do is create a signal on the c++ page and map that to the QML function, so when the c++ signal is activated, it activates the QML function.

Here is an example of a QML function, which is basically a JavaScript function.

onActionDone() {   // do some action}

Simple enough.  And here is what you need for creating a signal on the .cpp page.

signal:   void actionDoneSignal();

Again, pretty simple.  And here is how you connect the two on the QML page.

mycpp.actionDoneSignal(onActionDone);

Very simple.  Now, the “mycpp” part is handled by registering the cpp class on the main.cpp;

qmlRegisterType<MyCPP>("MyCPP", 1, 0, "MyCPP");

And then importing the class and attaching it on the QML page.  I found this much easier than creating an abstract object and then calling it.  There might be a few more lines of code, but the IDE seems to like it better.

import MyCPP 1.0
attachedObjects: [
	MyCPP{
		id: mycpp
	}
]

That would be it, if I was doing this from the main QML page, but I’m not.  On my main QML page I have a TabbedPane  and I need for that main page to run the function that I call from one of it’s tabs.  Therefor, you need to make sure you can call the TabbedPane from the Tabs.  You do this by setting what I consider to be a global QML variable.  The best time to set it is when the main page has completed loading.

onCreationCompleted: {
	Qt.mainTab = mainTab;
}

Now, I can call the JavaScript function from the Tab after the signal is triggered by setting the connection on the Tab to connect to the function on the main QML.

mycpp.actionDoneSignal(Qt.mainTab.onActionDone);

I am setting the connection on the child Tab, because in this case, I want to make sure the connection happens after the other steps have completed.

This took me a while to figure out, but now I can use this method for other data sends and requests.

main.qml

import AuthServices 1.0
TabbedPane {
	id: mainTab
	objectName: "mainTab"
	showTabsOnActionBar: true
		Tab {
			id: loginTab
			objectName: "loginTab"
			title: qsTr("Login") + Retranslate.onLocaleOrLanguageChanged
			description: "Application Authentication"
			LoginPage { }
		}

		//Tab 1...2...3..

	onCreationCompleted: {
		Qt.mainTab = mainTab;
	}
	onActionDone() {
		// do some action
	}
}

LoginPage.qml

import MyCPP 1.0

Page {
	Container {
			Button {
				id: btnGeAuthenticate
				text: "Get OAuth Access"
				onClicked: {
				startOAuth();
			}
		}
		function startOAuth() {
		// actually last part of step
		mycpp.getAccess(accessURL, consumerKey, consumerSecret, returnCode, redirectURI, "authorization_code")
		mycpp.actionDoneSignal(Qt.mainTab.onActionDone);
	}
	attachedObjects: [
		MyCPP{
			id: mycpp
		}
	]
}

main.cpp

qmlRegisterType<MyCPP>("MyCPP", 1, 0, "MyCPP");

MyCpp.hpp

class MyCpp: public QObject
{
	Q_OBJECT
	public:
		AuthServices();
		virtual ~AuthServices() { }
	Q_SIGNALS:

	public Q_SLOTS:
		void getAccess(const QString &urlString, const QString &consumerKey, const QString &consumerSecret, const QString &code, const QString &redirectURI, const QString &grantType);

	private slots:
		void onGetReply();
	private:

	signals:
		void authenicationDone();
};

MyCpp.cpp


void MyCpp::getAccess(const QString &urlString, const QString &consumerKey, const QString &consumerSecret, const QString &code, const QString &redirectURI, const QString &grantType)
{
 // send request
}

void MyCpp::onGetReply()
{
 // handle request reply
 // emit
 emit actionDone();
}

OAuth2 with WebView and C++ call

I have been struggling with figuring out OAuth connection for an application I am trying to create for BB10.  There are examples for WebWorks and Native in the GitHub repositories, but they weren’t very straight forward.  And there were hints and posts that it could be done in a more simple method using WebView.  After looking at a bunch of different code, I finally figured it out and not it seems easy.  The key was to use the WebView for the initial call for OAuth to get a code to send for the final Access authentication and then use C++ to make a post call to get the access tokens.

WebView basically puts a web browser in your application, so you need to read information from the browser.  In my app, I checked to see if the url changed in the WebView.  And when the URL changed, I checked to see if the parameter “code” was included in the URL.  From there, I took the value and passed it into the C++ function.

Here is the basic Page that starts the call with a button to change the URL for the WebView.

import bb.cascades 1.0
import HelloWorldServices 1.0

Page {
	property string returnCode;
	property string consumerKey: "123456789abcdef";
	property string consumerSecret: "abcdef987654321";
	property string redirectURI: "http://mysite.com/oauth/";
	property int urlChange: 0;
	property bool codeReceived: false;
	property bool accessSent: false;
	property bool accessReceived: false;

	Container {
		Label {
			id: lHelloLabel
			// Localized text with the dynamic translation and locale updates support
			text: qsTr("Hello World") + Retranslate.onLocaleOrLanguageChanged
			textStyle.base: SystemDefaults.TextStyles.BigText
		}
		Button {
			id: btnGetHello
			text: "Get OAuth Access"

			onClicked: {
				startOAuth();
			}

			function startOAuth() {
				// open the authorzation url
				var url = 'https://secure.site.com/oauth2/authorize?'
				+ 'client_id=' + consumerKey
				+ '&response_type=code'
				+ '&redirect_uri=' + redirectURI;
				webWindow.url = url;
			}
		}

		ScrollView {
			Container {
				WebView {
					id: webWindow

					onUrlChanged: {
						var returnURL = webWindow.url.toString();
						// check to see if code has comeback before
						if(!codeReceived) {
							// Past initial login request
							if(returnURL.indexOf("code")) {
								// successful return
								var startTrim = returnURL.indexOf("code") + 5;
								var endTrim = returnURL.indexOf("&");

								if(endTrim > startTrim) {
									returnCode = returnURL.substring(startTrim, endTrim);
								}
								if(returnCode.length > 1) {
									// set code recieved to true
									codeReceived = true;
									if(!accessSent) {
										accessSent = true;
										// call Access function to complete OAuth
										getAccess();
									}
								}

							} else if(returnURL.indexOf("invalid_request") != -1) {
								taHelloResults2.setText("The request was malformed or missing parameters");
							} else if(returnURL.indexOf("unauthorized_client") != -1) {
								taHelloResults2.setText("The client is not authorized");
							} else if(returnURL.indexOf("access_denied") != -1) {
							  taHelloResults2.setText("The user denied the request for authorization");
								// do some sort of action about user not being able to access data
							} else if(returnURL.indexOf("unsupported_response_type") != -1) {
								taHelloResults2.setText("Meetup doesn't support the provided response_type");
							} else if(returnURL.indexOf("invalid_grant") != -1) {
								taHelloResults2.setText("The provided code was invalid");
							} else if(returnURL.indexOf("unsupported_response_type") != -1) {
								taHelloResults2.setText("Meetup does not support the provided grant type");
							} else {
								// other
								taHelloResults2.setText("Other data" + returnURL);
							}
						}

					}

					function getAccess() {
						var accessURL = String("https://secure.site.com/oauth2/access");
						hws.getAccess(accessURL, consumerKey, consumerSecret, returnCode, redirectURI, "authorization_code");
					}
				}
				TextArea {
					id: taHelloResults1
					minHeight: 50
				}

				TextArea {
					id: taHelloResults2
					minHeight: 50
				}
			}

		}
	 }
	attachedObjects: [
		HelloWorldServices {
		id: hws
    }
   ]
}

After using the Webview to get the initial request key, I call a C++ function to send an access request.

I pass in the url for the access request, my apps consumer key and secret, the returned code from the initial request, the redirect url (required for the OAuth I’m using, but does nothing) and the grant type.

void HelloWorldServices::getAccess(const QString &urlString, const QString &consumerKey, const QString &consumerSecret, const QString &code, const QString &redirectURI, const QString &grantType)
{
	QUrl mMultiPart;
	mMultiPart.setUrl(urlString);
	// post items for the Oauth Access I am using
	mMultiPart.addQueryItem("client_id", consumerKey);
	mMultiPart.addQueryItem("client_secret", consumerSecret);
	mMultiPart.addQueryItem("grant_type", grantType);
	mMultiPart.addQueryItem("redirect_uri", redirectURI);
	mMultiPart.addQueryItem("code", code);
	// blank outrequest
	QString outRequest = "";

	QNetworkAccessManager *networkManager = new QNetworkAccessManager(this);
	// Setting mutipart query to request
	QNetworkRequest request(mMultiPart);
	request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
	// ssl setup - works with or without
	QSslConfiguration config = request.sslConfiguration();
	config.setPeerVerifyMode(QSslSocket::VerifyNone);
	config.setProtocol(QSsl::TlsV1);
	request.setSslConfiguration(config);

	if (networkManager) {
		QNetworkReply *reply = networkManager->post(request, outRequest.toAscii());
		bool ok = connect(reply, SIGNAL(finished()), this, SLOT(onGetReply()));

		Q_ASSERT(ok);
		Q_UNUSED(ok);

		if (reply) {
			qDebug() << "Reply from server is " << reply->readBufferSize();
		}
	}
}

When the reply happens, then I can parse out the JSON response

void HelloWorldServices::onGetReply()
{
	QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
	QString response;

	if (reply)
	{
		if (reply->error() == QNetworkReply::NoError) {
			int available = reply->bytesAvailable();
			if (available > 0) {
				QByteArray buffer(reply->readAll());
				response = QString(buffer);
			} else {
				response = "Zero bytes in response";
			}
		} else {
			int httpStatus = reply->attribute(
			QNetworkRequest::HttpStatusCodeAttribute).toInt();
			response = "Error and the code is "
			+ reply->attribute(
			QNetworkRequest::HttpStatusCodeAttribute).toString()
			+ " And the Error string is :: "
			+ reply->errorString() + "\n";
		}
		reply->deleteLater();
	}  else  {
		response = "Response comes out to be null";
	}

	if ((response.trimmed().isEmpty()))
	{
		response = "Empty Response";
	}
	qDebug() << "Response String is :: " << response << " \n";

	if (!response.isNull())
	{
		qDebug() << "Start JSON";
		JsonDataAccess jda;
		QVariant mainList = jda.loadFromBuffer(response);
		qDebug() << mainList.toStringList();
		qDebug() << mainList.toMap().value("access_token");
		qDebug() << mainList.toMap().value("token_type");
		qDebug() << mainList.toMap().value("expires_in");
		qDebug() << mainList.toMap().value("refresh_token");
	}
}

DataModel Change Trigger

As I am working through creating a minimum application template, I want to make sure I have the features that will be needed in almost any application.  One of those would be to trigger a notification that the DataModel has been updated.  An example use of this is a ListView populated using a DataModel.  In the application I created for work, when I add a new sales lead through the form, the user gets sent back to a ListView of all the entered leads saved in the device sql database.  After the data is inserted into the table successfully, then the dataModelChanged trigger which “refreshes” the datamodel.  When the datamodel is “refreshes”, then the ListView that is built off of the datamodel “refreshes” and you see the new or updated record.   While this wasn’t obvious on the application during a save, it was obvious when the lead was sent to the web service, because I had the ListItemComponent status value change from the lead id to “Saved”.  That takes another little trick using a javascript function, which I will post about later.

The setup for the datamodel trigger is fairly straight forward.  First, you need to add the datamodel to the class as a property and as an accessible parameter.  In my application, since the applicationui was the main class, the datamodel was added there.  You also need to have a parameter to call the the data in the model attached to the class.

In the class header (applicationui.hpp).


class ApplicationUI : public QObject
{
Q_OBJECT

// A property that is used by the list view in QML
Q_PROPERTY(bb::cascades::DataModel* dataModel READ dataModel NOTIFY dataModelChanged CONSTANT)

public:
  ApplicationUI(bb::cascades::Application *app);
  ApplicationUI();
  virtual ~ApplicationUI() { }

  Q_SIGNALS:
    void dataModelChanged();
  public Q_SLOTS:
  private Q_SLOTS:
  private slots:
  private:
    // The getter method for the property
    bb::cascades::GroupDataModel* dataModel() const;
    // The data shown by the list view.
    bb::cascades::GroupDataModel* m_dataModel;
};

Then you need to make sure you initiate the data model.  In the example below, the “leadID” is being used to sort the data model group.  When you do an create or update call, you need to trigger that the data model has changed by simply call “emit dataModelChanged()”, which was set in the class head property.  And, most importantly, you need to read in the data to the data model, so that there is something to see.  In this example, there is a class called Person that holds each lead record, which is then added to the data model.

In the class source (applicationui.cpp)


ApplicationUI::ApplicationUI(bb::cascades::Application *app) :
QObject(app),
m_dataModel(0), m_lastLeadID(0), m_succeeded(false), m_active(false)
{
  initDataModel();
  // Initialize the database, ensure a connection can be established  and that all the required tables and initial data exists
  const bool dbInited = initDatabase();
  if(dbInited) {
    readLeadRecords();
  }
}
void ApplicationUI::initDataModel()
{
  // Note: The Group Data Model is joining this objects tree as a child (for memory management)
  m_dataModel = new GroupDataModel(this);
  m_dataModel->setSortingKeys(QStringList() << "leadID");
  m_dataModel->setSortedAscending(false);
  m_dataModel->setGrouping(ItemGrouping::None);
}
bool ApplicationUI::createLeadRecord(const QString &firstName,...)
{
  .... do sql insert
  emit dataModelChanged();
  return success;
}

bool ApplicationUI::updateLeadRecord(const QString &firstName,...)
{
  .... do sql update
  emit dataModelChanged();
  return success;
}
void ApplicationUI::readLeadRecords()
{
  .... do sql select
  // The data will be displayed in a group data model
  // Clear any previous reads from the data model first
  m_dataModel->clear();
  while (query.next()) {
    // create person object for insert into group data model
    Person *person = new Person(query.value(0).toString, ....);
    // insert into group data model
    m_dataModel->insert(person);
  }
}

Finally, you can setup the ListView in a Page Container and point the dataModel to the one referenced in the main class header.


import bb.cascades 1.2
//! [0]
Page {

  Container {
    bottomPadding: 30

    ListView {
      id: leadsList
      horizontalAlignment: HorizontalAlignment.Fill
      dataModel: _app.dataModel
    }
  }
}

The issue I had with my application was that I had a separate class for the web service calls.  On the web service result, I wanted to update the database to set a field “savedToServer” to 1 (true).  I had no problem doing that, but I couldn’t call the data model updated call from that class.  In the end, I moved my web services calls to the main class, but that’s not how I want things to work.  So, hopefully, with my minimum template setup, I can call things between classes without any issue.

BB10 Cascades Minimum Template

While getting to learn BB10 Cascades and Native, one of the most annoying issue is trying to figure out where to start with the project. When you create a new BlackBerry Project using QNX Momentics IDE, you can select from either Native or Cascades. From there, you can select different levels of templates. For the Cascades templates, you can either choose Standard empty project, List View, Tabbed Pane or Navigation Pane. This would be fine if you were going to do everything needed just in Cascades. But, (I’m new to this, so I could be wrong), if you want to add c++ function calls from the QML pages, the templates are a little lacking. What makes it even more confusing is going to the Cascade Examples on GitHub and following along with what they did, which doesn’t have the file and code layout given from the templates. So, I decided to see what would be the minimum setup in order to call a simple c++ function from a qml page.

I started with the Standard empty project and used the Sensor Demo as a guide for getting the basic layout.

After creating the project, the first thing to do is fix the file names for the c++ section.  In the examples, they use file names that are similar to the name of the project.  I don’t know why the IDE doesn’t do this, but it also doesn’t setup the project the same. Since I name the project HelloWorldMinimum, I will name the class files HellowWorldServices.

Rename the mail c++ files to match closer to the project name

Rename the mail c++ files to match closer to the project name

You also have to make sure you rename the include call in the newly name HelloWorldServices.cpp to reflect the new header file name.  This change is also needed in the main.cpp file as well.


#include "HelloWorldServices.hpp"

Next you will need to rename the classes within the both source and header file to match the new name.  You would think this would be done automatically when you rename the files, but I guess there’s a reason that they aren’t.  Also, part of this minimal setup is making the main.cpp file actually the main class.  The application calls in the original applicationui.cpp and application.hpp need to be removed and replaced by simple QObject type calls to the parent.

Rename the class and change the function and types

Rename the class and change the function and types


class HelloWorldServices : public QObject
{
   Q_OBJECT
public:
   HelloWorldServices(QObject *parent = 0);
   virtual ~HelloWorldServices() { }

Finally, the Translation function needs to be moved to the main.cpp as well as declaring the root of the application.


Application app(argc, argv);

// localization support
QTranslator translator;
const QString locale_string = QLocale().name();
const QString filename = QString::fromLatin1("HelloWorldMinimum_%1").arg(locale_string);
if (translator.load(filename, "app/native/qm")) {
  app.installTranslator(&translator);
}

QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(&app);
AbstractPane *root = qml->createRootObject<AbstractPane>();

app.setScene(root);

return Application::exec();

When everything looks okay (I’ll have the complete code below), then you can build the project.  You should get an error reminding you that you forgot to clean the project of the old pointers to the old file names.

Fix the errors by cleaning the project and building again

Fix the errors by cleaning the project and building again

Simply choose Clean Project and the Build Configurations > Build All to build the project again.

From here you can put the hook into call the class in the main.cpp.


qmlRegisterType<HelloWorldServices>("HelloWorldServices", 1, 0, "HelloWorldServices");

Once this is there, then you can easily call any function in the class from the qml page.  In my example, I simply put in a call to a getHello() function from the class that will return a QString value of “Hello there!”  After registering the HellowWorldServices, I can easily import it the qml and IDE recognizes it for the code help.


import HelloWorldServices 1.0

After it is imported, then you need to attach it as an object, so that you can call the functions.


onClicked: {
taHelloResults.text = hws.getHello();
}

That’s it!  I plan on seeing if calling c++ classes from another c++ class is now easier with this setup and I’ll make a post when I figure it out.  For now, here is the full code for each page.

main.cpp


#include "HelloWorldServices.hpp"

#include <bb/cascades/Application>
#include <bb/cascades/QmlDocument>
#include <bb/cascades/AbstractPane>

#include <QLocale>
#include <QTranslator>

#include <Qt/qdeclarativedebug.h>

using namespace bb::cascades;

Q_DECL_EXPORT int main(int argc, char **argv)
{
  Application app(argc, argv);

  qmlRegisterType<HelloWorldServices>("HelloWorldServices", 1, 0, "HelloWorldServices");

  // localization support
  QTranslator translator;
  const QString locale_string = QLocale().name();
  const QString filename = QString::fromLatin1("HelloWorldMinimum_%1").arg(locale_string);
  if (translator.load(filename, "app/native/qm")) {
    app.installTranslator(&translator);
  }

  QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(&app);
  AbstractPane *root = qml->createRootObject<AbstractPane>();

  app.setScene(root);

  return Application::exec();
}

HelloWorldServices.cpp


#include "HelloWorldServices.hpp"

#include <bb/cascades/Application>
#include <bb/cascades/QmlDocument>
#include <bb/cascades/AbstractPane>
#include <bb/cascades/LocaleHandler>

using namespace bb::cascades;

HelloWorldServices::HelloWorldServices(QObject *parent) :
   QObject(parent)
{

}

QString HelloWorldServices::getHello()
{
  return QString("Hello there!");
}

HelloWorldServices.hpp


#ifndef ApplicationUI_HPP_
#define ApplicationUI_HPP_

#include <QObject>

namespace bb
{
  namespace cascades
  {
    class Application;
    class LocaleHandler;
  }
}

class HelloWorldServices : public QObject
{
  Q_OBJECT
  public:
  HelloWorldServices(QObject *parent = 0);
  virtual ~HelloWorldServices() { }

  public Q_SLOTS:
  QString getHello();

  private slots:

  private:

};

#endif /* ApplicationUI_HPP_ */

main.qml


import bb.cascades 1.2
import HelloWorldServices 1.0

Page {
  Container {
  //Todo: fill me with QML
    Label {
    // Localized text with the dynamic translation and locale updates support
      text: qsTr("Hello World") + Retranslate.onLocaleOrLanguageChanged
      textStyle.base: SystemDefaults.TextStyles.BigText
    }

    TextArea {
      id: taHelloResults
      minHeight: 500
    }

    Button {
      id: btnGetHello
      text: "Test Hello"

      onClicked: {
         taHelloResults.text = hws.getHello();
      }
    }
  }
  attachedObjects: [
      HelloWorldServices {
         id: hws
      }
   ]
}