It's Coming Home Jul 14 2018

I've been watching England a long time now - I don't remember Italia '90 first hand, I do remember USA '94 as my first World Cup, even though England hadn't qualified. Euro '96 was special, and I vividly remember the semi-final against Germany, the elation, the agony, the heartbreak. Gazza sliding in at the back post. The first time I cried after an England result (not the last). Beckham red card and losing to Argentina in '98, being 1-0 up against Brazil in '02 and losing 2-1, penalties to Portugal in '06. After that, England seemed to be in the footballing wilderness, the so-called Golden Generation had come and gone, nothing to show for it. Overpaid and not really caring, the last decade has been tough to watch. Superstars picked based on reputation not form, and not delivering for country like they did for club. And so it comes full circle, watching this World Cup with my kids, the first World Cup where they were old enough to understand it, to be excited by it. Living vicariously through them, nervous for them, as they learn the highs and lows of being an England fan. Ultimately, perhaps inexperience delivered the final blow, but in doing so, has given these young, energetic and hungry players experience they can use in the future. In two years for the Euros, in four years for the next World Cup. Many of these players will play in 2 or maybe even 3 more World Cups if they stay fit and consistent, and this experience, this team unity, the exuberance they've shown together will count for so much. They've set themselves a high bar for future years, and in doing so have inspired my kids, and kids up and down the country. Keep the faith, because eventually, It's Coming Home.

Comments

Announcing Dotplan Feb 18 2018

I've re-focused how I keep track of notes and thoughts for ideas and plans, and in so doing, given an update to an old concept.

In Masters of Doom, it details how John Carmack, genius programmer at id software, used .plan files to keep track of his todos and work plans when building out classic games like Wolfenstein 3d, Doom, and Quake. He eventually started to make these public, as a way of feeding back to his community and fanbase, exactly what he was up to.

Introducing Dotplan, a very simple content system based upon the excellent Middleman framework used for Ruby content management. It's a default Middleman blog style setup, allowing unlimited plans to be created. You can simply clone it and hit the ground running, and write all your plans in Markdown - build the site into HTML, and ship to your favourite static host or CDN. Or keep it all to yourself, and simply run it locally as a journal to keep you sane!

There are two ways of utilising Dotplan - if you like the idea of just writing, then clone the repository and do just that. All the plans content is by default excluded from the source control, meaning you can continue to pull down Dotplan updates as it changes, and you won't have to worry about merging or conflicts. Just make sure to back up your content through some other means - perhaps using Dropbox or another similar solution. It's a clean separation of the code to power the tool, and your content.

Alternatively, fork the repo and clone your fork, then feel free to remove the line in .gitignore which stops plans from being included. You can then check them into source control to make sure they are safe. The advantage is you can more easily track changes to your content, roll things back etc - the downside is it may make it slightly harder to pull down upstream Dotplan changes going forward.

However you do it, Dotplan is a simple way to start getting organised - or at the least, get all of those creative thoughts out of your head and down somewhere. That gives you the headspace to prioritize and focus, and makes sure you can always refer back to them in the coming weeks or months.

Check out Dotplan here.

Comments

Tic-Tac-Toe with Vue.js Jan 17 2018

Following on from our look at setting up the Vue.js and React frameworks in Rails 5.1, this post will look at building a small little component in Vue.js, strictly on the client-side (just HTML, JS and CSS, no backend Rails app!).

We're going to implement our own little version of the classic Tic-Tac-Toe game, called Tic-Tac-Vue.

Setup

We'll start by creating our structure - create a folder, an HTML page, a JS file, and a stylesheet.

mkdir tic-tac-vue
cd tic-tac-vue
touch index.html
touch app.js
touch app.css

Our HTML file is going to be very basic, let's run through our initial markup:

<html>
  <head>
    <title>Tic Tac Vue</title>
    <script src="http://unpkg.com/vue"></script>
    <link rel="stylesheet" href="./app.css" />
  </head>
  <body>
    <script src="./app.js" type="text/javascript"></script>
  </body>
</html>

This sets up our page, brings in Vue.js, our CSS and our own JS file (both currently empty).

Next up, we're going to add a root element for our app to live - this goes in our HTML body, above our script reference to our JS file:

<div id="app"></div>

And now we need the markup for our game grid itself, which we're going to put in a script tag with a special template type - this too goes in our body, and above our JS file script ref:

<script type="text/x-template" id="grid">
  <div class="grid">
    <div class="row" v-for="row in rows">
      <div class="column" v-for="column in row">
        {{column}}
      </div>
    </div>
  </div>
</script>

Let's run through that so it's clear - our grid is made up of rows and columns, fairly simple. It'll be based on a 2D array representing that data, and so we're looping through our rows data, which is an array of arrays, and then looping through each individual array, which contains the values for our individual columns. We'll be expecting three rows, and three columns for Tic-Tac-Toe of course. The column values will either be blank (unplayed), or an x or an o if someone has played in that space. That's it!

Quick note: Why do we separate the markup like this? You could include that markup within the #app div directly when the app is so simple, however there are still a couple of downsides to this. The first is that you have to then be careful which markup you're using - the browser will process that as HTML within the DOM long before Vue gets to it, meaning that if you then start to use any markup specific to how Vue works (like referencing components with tags) that isn't HTML5 compliant, the browser could end up stripping it out, or doing odd things with it that'll affect how your app works. Additionally, most apps are going to have enough complexity to have at least one component (rather than defining the app as just a root Vue app), so it makes sense to define that as such from the beginning, as it's neater and clearer to follow, as well as more cleanly allowing for expansion as the need arises. We've separated our page container markup (to host our client-side app) from the app itself, which is comprised of our specific Vue markup template, and the JavaScript we'll be writing to implement the functionality itself.

With our HTML out of the way, let's format our grid so it looks like one - here is our CSS:

.row {
  clear: both;
}
.column {
  border: 1px solid #000;
  width: 50px;
  height: 35px;
  float: left;
  text-align: center;
  padding-top: 15px;
}

Fairly simple - we're floating each column and having them on the same row, with each row clearing the float to move on to the next row - we setup a border for the grid itself, set a specific height and width, and make sure that if a text value is shown (x or o) it looks nice within the box for that space on the game grid.

Now we get to the fun part - our JavaScript! We have two tasks now to get our initial grid functional - we have to create and define our grid as a component, and we then have to create our Vue app with our grid component being rendered into our #app empty div above.

First, we define our grid component:

var Grid = Vue.component('grid', {
  template: '#grid'
});

This is easy enough to follow - we define a component, named Grid, using the template with id grid, and store it as Grid for reference elsewhere.

We're going to need to set our initial data for the component though - the rows and columns that the template renders:

data: function() {
    return {
      rows: [
        ['', '', ''],
        ['', '', ''],
        ['', '', '']
      ]
    };
  }

As we described above for our template, rows is a 2D array, an array of arrays, each array representing a row, and each value within representing a column in that row - and we're starting with them all blank and unplayed.

Next up, let's define our App:

var App = new Vue({
  el: '#app',
  components: {
    Grid
  },
  render: function(createElement) {
    return createElement(Grid);
  }
});

Here we specify the element our app will reside in, so #app, and then the components we need to use - we just have one, our Grid component. Lastly, as we don't have any markup within our #app element to tell our app how and what to render, we do it with a custom render method, referencing our game grid component.

Open up index.html now in a browser and you'll see our grid! That's great, but it doesn't do a lot. Let's start making it so we can play the spaces, and get a game going!

Game on

First of all, we want the user to be able to select a space, and if it's empty, it'll place an x or an o in it, depending upon whose turn it is. Let's add a variable to our data to determine whose go it is, and which one is next:

  data: function() {
    return {
      rows: [
        ['', '', ''],
        ['', '', ''],
        ['', '', '']
      ],
      next: 'x'
    };
  },

So we've just added that next attribute with a default value of x to our component data.

Now we'll add a method to our component definition, that'll handle when a space is tapped:

methods: {
  tap: function(e) {
    if (e.target.innerText == '') {
      e.target.innerText = this.next;
      this.next = (this.next == 'x' ? 'o' : 'x');
    }
  }
}

So it only works if the space is free, sets the space, and then toggles which player is next. Reload our page, tap a space, and… nothing. That's because we have one final step - we need to hook up tapping on the game grid spaces to that method we just wrote!

Change the markup in our #grid component template, so it's like this:

<div class="column" v-for="column in row" v-on:click="tap">
  {{column}}
</div>

The v-on:click attribute is added, which adds an event handler for the click event to the component, pointing at the tap method. Try reloading again now, et voila! Tapping on spaces sets the next player character in the space, with no overwriting involved. Pretty neat, and not a lot of code either!

Data binding

We have our action in place, but we can't do much with spaces played as we're not updating our data based on the user input - we're just directly updating the UI. Instead, let's update our data array, and have that reflect to the UI, which is a much better way to handle things.

In our markup, we'll alter our Grid component template as follows:

<script type="text/x-template" id="grid">
  <div class="grid">
    <div class="row" v-for="(row, row_index) in rows">
      <div class="column" v-for="(column, column_index) in row" v-on:click="tap" v-bind:data-row="row_index" v-bind:data-column="column_index">
        {{column}}
      </div>
    </div>
  </div>
</script>

Specifically, we're keeping track of the row and column index in our two-dimensional array looping, and then binding it as row and column data variables to reference when a space is played.

Over to our tap method now:

tap: function(e) {
  if (e.target.innerText == '') {
    let rows = this.rows;
    rows[e.target.attributes['data-row'].value][e.target.attributes['data-column'].value] = this.next;
    this.rows = rows.slice(0);
    this.next = (this.next == 'x' ? 'o' : 'x');
  }
}

Here we are grabbing the data rows, then updating the value of our column, using the data-row and data-column bindings present on the element tapped to find the right location in the array. The reason we do a slice(0) is because the way that the data binding works in Vue, the rows property in our data is reactive (meaning it is being watched for changes), but individual changes within the array won't trigger that watcher, and therefore a re-render. Instead we're using a local rows variable, and then effectively copying the whole array back into our data, which triggers our reaction and an update of the UI using our template (rather than us modifying the element directly).

So now we have our app data binding correctly, we can implement logic based on the current rows/columns data to check for win criteria!

Winner winner

To check for a winner, we need to check each row for three matching values, each column for three matching values, and the two diagonal paths for three matching values. If any of those are true, we have a winner! We can boil this down to the following:

checkWinner: function() {
  return (
    this.checkValues(this.rows[0]) ||
    this.checkValues(this.rows[1]) ||
    this.checkValues(this.rows[2]) ||
    this.checkValues([this.rows[0][0], this.rows[1][0], this.rows[2][0]]) ||
    this.checkValues([this.rows[0][1], this.rows[1][1], this.rows[2][1]]) ||
    this.checkValues([this.rows[0][2], this.rows[1][2], this.rows[2][2]]) ||
    this.checkValues([this.rows[0][0], this.rows[1][1], this.rows[2][2]]) ||
    this.checkValues([this.rows[0][2], this.rows[1][1], this.rows[2][0]]));
},
checkValues: function(values) {
  return (values[0] != '' && values[1] != '' && values[2] != '' && (values[0] === values[1]) && (values[1] === values[2]));
}

Add those methods to our component underneath our tap definition.

checkValues is easy enough - we expect an array of three values, we make sure all the values are not blank, and that they are the same. checkWinner then calls checkValues for all of our different possible value arrays - firstly checking each of the three rows (each of those is already an array with three elements), then the three columns, and then lastly, the two diagonals.

With our win logic in place, we now just need to hook it up. We'll add an additional property, finished, to our data:

data: function() {
  return {
    rows: [
      ['', '', ''],
      ['', '', ''],
      ['', '', '']
    ],
    next: 'x',
    finished: false
  };
},

We'll then reference that to make sure that you can't keep playing once we're finished - at the top of the tap method:

tap: function(e) {
  if (!this.finished && e.target.innerText == '') {

Lastly in terms of logic, we just need to amend the end of our tap method:

if (this.checkWinner()) {
  this.finished = true;
} else {
  this.next = (this.next == 'x' ? 'o' : 'x');
}

So we only go to our next player if we've not found a winner - if we have found a winner, we mark the game as finished.

Playing the game now, we'll see that if one player wins, you can't continue playing. But some kind of feedback to indicate that the game is over (and to say who has won!) would be good. Easy! We can do that with a single additional line in our template - at the bottom within our .grid div, below the rows & columns, add:

<div class="status" v-if="finished">Finished! {{next}} wins!</div>

Add the following to app.css to make sure our message sits underneath the grid:

.status {
  clear: both;
}

Now when someone wins and the game stops, you can see who has won!

Whose turn?

It'd be good if we could show whose turn it is next, in case the players lose track. We already have the data, and as you can probably guess from how we displayed the finish state, it's fairly easy to do - change our .status text div to the following:

<div class="status">
  <span v-if="finished">Finished! {{next}} wins!</span>
  <span v-else>Next go: {{next}}</span>
</div>

Here we're making use of v-if and v-else to make sure only one shows - either we're showing whose turn is next, or we show the finished/winner text as the game is over!

Replayability

We're almost there with our incredible take on Tic-Tac-Toe, but rather than having to refresh the page after every game to go again, wouldn't it be cool if we could just press a restart button instead?

We're going to move our code to determine the next player into its own method, so we can re-use it:

nextPlayer: function() {
  this.next = (this.next == 'x' ? 'o' : 'x');
},

Next up, we'll change the tap method to use that function - so for the winner check or next player logic:

if (this.checkWinner()) {
  this.finished = true;
} else {
  this.nextPlayer();
}

Now, we'll implement our restart method:

restart: function(e) {
  this.rows = [
    ['', '', ''],
    ['', '', ''],
    ['', '', '']
  ];
  this.finished = false;
  this.nextPlayer();
},

This just resets the rows, makes sure the game is no longer finished, and triggers the next player. Let's hook that up to something in the template now:

<div class="status">
  <div v-if="finished">
    <p>Finished! {{next}} wins!</p>
    <a v-on:click="restart">Restart</a>
  </div>
  <span v-else>Next go: {{next}}</span>
</div>

Our restart link shows when the game is finished, and tapping it resets our game accordingly!

Stalemate

The last thing we need to handle, is the dreaded stalemate. When two world class Tic-Tac-Toe players are locked together, head-to-head, neither willing to budge an inch, we could end up with a tied game. What then? The game doesn't get marked as finished, but there are no more moves to make. The games cannot continue!

So let's add a check for that state, and handle it accordingly. We'll start by refactoring our checkValues method, so we can re-use the blank check within it for checking if all the spaces have been played:

checkValues: function(values) {
  return this.checkValuesPresent(values) && this.checkValuesMatch(values);
},
checkValuesPresent: function(values) {
  return (values[0] != '' && values[1] != '' && values[2] != '');
},
checkValuesMatch: function(values) {
  return (values[0] === values[1]) && (values[1] === values[2]);
}

As you can see, we've separated the two parts of checkValues into a check for whether the specified values are all present (checkValuesPresent) and whether the values all match (checkValuesMatch). Now we can use the checkValuesPresent check in our stalemate logic - add the following method:

checkStalemate: function() {
  return !this.finished &&
    (this.checkValuesPresent(this.rows[0]) &&
    this.checkValuesPresent(this.rows[1]) &&
    this.checkValuesPresent(this.rows[2]));
},

We're just making sure that the game isn't yet finished, and all of the rows have values present - then it must be a stalemate!

We'll add an additional property to track stalemates, so add the default value to our data:

next: 'x',
finished: false,
stalemate: false

And then we'll update our tap method to account for checking for stalemates:

if (this.checkWinner()) {
  this.finished = true;
} else if (this.checkStalemate()) {
  this.stalemate = true;
  this.finished = true;
} else {
  this.nextPlayer();
}

We have to check for a stalemate after a win, because there might be a winner on the very last space! If it is a stalemate, the game is finished, but also the stalemate property is set.

We can now use this on the frontend within the template to display something more relevant:

<div class="status">
  <div v-if="finished">
    <p v-if="stalemate">It's a draw! Stalemate!</p>
    <p v-else>Finished! {{next}} wins!</p>
    <a v-on:click="restart">Restart</a>
  </div>
  <span v-else>Next go: {{next}}</span>
</div>

We're either displaying our draw statement for stalemates, or the winner text, and the restart button either way so a new game can start. The only thing left, is to make sure our stalemate flag is reset on restart:

restart: function(e) {
  this.rows = [
    ['', '', ''],
    ['', '', ''],
    ['', '', '']
  ];
  this.finished = false;
  this.stalemate = false;
  this.nextPlayer();
},

Now win, lose or draw, players can continue playing!

Wrapping up

So our Tic-Tac-Toe game is pretty much feature complete, at least in terms of a basic game two people can play on a single computer. It saves on paper and drawing, and we've done it all with just a little HTML + CSS, and a bit of fun JavaScript, all thanks to Vue.js!

This is one way to build a Vue.js app, and is better suited for small, self-contained ideas like this. In a future post we'll look at going back to Webpack and using vue-cli, so that we can do things like having each component of our app (template HTML and JS logic) in its own .vue file, that's pre-processed accordingly to build the JS for our page. As projects increase in complexity, that helps to give them structure and keep them maintainable.

Check out the full code for this post at https://github.com/ejdraper/tic-tac-vue, and you can play the game itself below!

Comments

Frontend Fun with Rails 5.1 and Webpacker Jan 8 2018

Client side frameworks for more responsive web apps are en vogue, and have been for a number of years now. The frameworks themselves change in popularity - to begin with it was Backbone, then Ember and Angular - more recently, frameworks like Vue and React have become the most popular choices.

Let it never be said that Rails doesn't move with the times though, as Rails 5.1 introduced the Webpacker gem, providing a smarter interface to allow you to use Webpack config for bundling JavaScript packages on the client side of your Rails web app. This means easier access to stable versions of client side frameworks, and easier maintainability for those dependencies going forward. It's a separate gem too, allowing you to also use it in Rails 4.2+ apps if needed.

Let's take a look at just how easy it is in Rails 5.1 now to setup Vue.js and React. Make sure you have the latest version of Rails installed, and then we'll setup a new empty app:

rails new frontend-fun

You can also specify to use webpack as part of this command and skip the next couple of steps:

rails new frontend-fun --webpack

However, if you're looking to add this to an existing Rails app, these next two steps will allow you to add Webpacker ready to use.

Within the app, add the webpacker gem to Gemfile:

gem "webpacker"

Then run bundle from a terminal in the root directory of the project to install the gem:

bundle install

At this point, we'll now have some additional commands available to us courtesy of the webpacker gem, to install frontend packages. We'll get things rolling with the initial command to prep our webpacker config:

 bin/rails webpacker:install

With that done, we should probably setup a basic page for testing, so create a new controller app/controllers/site_controller.rb, with the following Ruby code in it:

class SiteController < ApplicationController
end

Then we'll create a template for our content, at app/views/site/index.html.erb:

<h1>FrontendFun</h1>

Finally, in our config/routes.rb file, we'll add a reference for the root of our app to the new controller and action:

Rails.application.routes.draw do
  root "site#index"
end

If you test our app now, you'll see the FrontendFun header. Last thing we now need to do is hook in our webpacker JS, with a reference from our layout template. In app/views/layouts/application.html.erb, below the javascript_include_tag reference in the <head>, add:

<%= javascript_pack_tag 'application' %>

Now if you reload the page, and look in the JavaScript console, you should see:

Hello World from Webpacker

Webpacker - it's alive!

Vue.js

To install Vue.js, run the following commands:

bin/rails webpacker:install:vue
bin/yarn install

This will install the Vue.js packages. We'll then hook up the Hello Vue pack that is setup by default, by modifying our javascript_pack_tag line in app/views/layouts/application.html.erb:

<%= javascript_pack_tag 'application', 'hello_vue' %>

Reload our page, and you'll now see Hello Vue! in our page body, which is coming directly from Vue.js!

React

Now, in a production application, you're unlikely to want to use more than one frontend framework, unless you were transitioning from one to another. But, this is FrontendFun, so we're going to add React too!

By now, this should be fairly straightforward, we run:

bin/rails webpacker:install:react
bin/yarn install

This will install the React packages now, and setup a base HelloReact component similar to our HelloVue. So we'll just need to modify our javascript_pack_tag one more time to reference our React pack:

<%= javascript_pack_tag 'application', 'hello_vue', 'hello_react %>

Once more, reload our page, and behold - Hello Vue! and Hello React!.

Two frontend frameworks, setup very easily, co-existing in the same HTML page in our Rails app. The possibilities are practically endless!

Next steps

From here, you can edit either app/javascripts/packs/hello_vue.js and app/javascripts/app.vue if you want to use Vue.js, or app/javascripts/hello_react.jsx for React, adding new components and interactions to build out your frontend, and integrate it with whatever backend functionality your Rails app might offer. You can check out the simple demo Rails app that we've just built up here.

Comments

Say hello to AppTrack Dec 21 2017

Say hello to AppTrack, a brand new utility for macOS that's available today on the macOS App Store!

What's AppTrack?

AppTrack is a macOS app that tracks the apps you have open on your Mac, and captures the time spent in each one, so you can see where you are spending the most time.

Knowledge is the first step

AppTrack will help you to identify bad habits, and adjust over time to being more productive when it counts. It's easy to overlook how much we can flick between apps, a quick look over at Twitter, or browsing for the perfect playlist in Spotify! Even if you're regimented with something like the Pomodoro technique, it can still be easy to overrun on the rest periods and get carried away browsing Reddit! Now you can quantify where your time is being spent, and resolve to make positive steps towards being more productive.

Where can I learn more?

You can visit the AppTrack page on the KickCode site that includes more details and screenshots here.

Where can I get it?

It's available right now on the macOS App Store for $2.99/£2.99!

Over to you

This is just the first version of AppTrack. I'll be looking to add features to AppTrack in subsequent versions, like being able to differentiate between websites in browsers so you can see how much of your Google Chrome time is Trello and GitHub, and how much is Facebook and YouTube! I welcome any feedback, suggestions, questions or comments though, so please let me know!

Comments

It's about time Nov 22 2017

Well well well. Over four years since my last post here! Time flies when you're having fun.

UPDATE: I've since brought across some of my writing from kickcode.com so there are a few more posts in the intervening time period now!

Looking back over the blog, I've done the whole "wow, it's been ages since I last posted, but I'm going to blog a lot more now!" post a fair few times. So what's one more?

As a bit of fun, here are some things that have happened since I last blogged here:

  • We had a third child!
  • Our eldest two children started school
  • I became a governor at the kids school, and have since gone on to become Chair of Governors
  • We all (all five of us!) started learning karate with a fantastic local club, grading a number of times, and competing successfully at a variety of competitions - in the case of my eldest two kids, recently competing at an overseas, international level World Championships event and bringing home bronze medals
  • I finally found a fitness routine that I enjoy and have been able to stick to, with the advice and guidance of a good friend, managing to lose 3.5 stone, and dropping to under 13% body fat in a little over a year
  • I ran a 10K race, clocking in at around 56 minutes
  • I got my first (and for the time being, only) tattoo
  • We returned to Vegas, where we got married ten years ago this year, for a few days break away
  • Worked on and delivered over 12 different web and mobile apps, some from scratch, and some where I was brought onboard to extend and improve them. Some were long term engagements, and others were small 6-8 week projects to deliver MVP (minimum viable product) prototypes
  • I've learnt React/React Native, the Go programming language, and played around with Rust too
Comments

Working with NSTableView Aug 18 2015

This article is a look at how to use NSTableView when building a Mac app using RubyMotion, and is taken from the Data chapter in my book, Building Mac OS X apps with RubyMotion. If you like what you read here, please take a look at my book - in the book we progress further with the following app code to look at Core Data, and then integrate with iCloud. You can also get a free 20+ page Getting Started guide.

A table view can be a good way to start with presenting data in your app, before you customize further or even replace it later if needed. It's a no-frills, easy to use way to get something up and running. We'll start by building a simple note taking app, that will just keep the notes in memory and display them in the table view. Create the app using the OS X template as we've done before:

motion create --template=osx Noteworthy
cd Noteworthy

First up, let's add in our favorite user interface gem, motion-kit:

gem 'motion-kit'

Then run the following:

bundle install

Then we'll create our layout file to setup our user interface - here is the code for app/main_layout.rb:

class MainLayout < MotionKit::Layout
  FORM_HEIGHT = 100
  TITLE_HEIGHT = 30
  CONTENT_HEIGHT = 70
  BUTTON_WIDTH = 70
  BUTTON_HEIGHT = 30

  def layout
    add NSView, :form do
      constraints do
        width.equals(:superview)
        height.is(FORM_HEIGHT)

        min_left.is 0
        min_top.is 0
      end

      add NSTextField, :title_text do
        placeholderString "Enter note title"

        constraints do
          width.equals(:superview)
          height.is(TITLE_HEIGHT)

          min_left.is 0
          min_top.is 0
        end
      end

      add NSTextField, :content_text do
        placeholderString "Enter note content"

        constraints do
          width.equals(:superview)
          height.is(CONTENT_HEIGHT)

          min_left.is 0
          min_top.is TITLE_HEIGHT
        end
      end

      add NSButton, :save_button do
        title "Save"

        constraints do
          width.equals(BUTTON_WIDTH)
          height.is(BUTTON_HEIGHT)

          min_right.is 0
          min_bottom.is 0
        end
      end
    end

    add NSScrollView, :scroll_view do
      has_vertical_scroller true

      constraints do
        width.equals(:superview)
        height.equals(:superview).minus(FORM_HEIGHT)

        min_top.is FORM_HEIGHT
      end

      table_view = add(NSTableView, :table_view) do
        row_height 25

        add_column "title" do
          title "Title"
          min_width 150
        end

        add_column "content" do
          title "Content"
        end
      end
      document_view table_view
    end
  end
end

Most of this should be fairly straightforward if you've previously read through my previous post on motion-kit - we're just setting up a window with the top section containing form fields for creating a new note, with a button to save it, and the bottom section is a table view inside a scroll view, so that when there is more content in the table than there is room in the window, you can scroll through it easily. We're setting up two columns in the table view, title and content.

After that, we'll update our AppDelegate, adding the following to the end of our applicationDidFinishLaunching:

@title_text = @layout.get(:title_text)
@content_text = @layout.get(:content_text)

@notes = []
@table_view = @layout.get(:table_view)
@table_view.delegate = self
@table_view.dataSource = self

@save_button = @layout.get(:save_button)
@save_button.target = self
@save_button.action = 'note_save:'

We're grabbing references to the controls we need to interact with, and then we're following that up by ensuring that the AppDelegate itself acts as both the delegate, and data source for our table view - we'll come back to that in a minute. Then we're setting up the save button to call a method on the AppDelegate, called note_save. That method looks like this:

def note_save(sender)
  title = @title_text.stringValue
  content = @content_text.stringValue
  return if title.nil? || title.empty? ||   content.nil? || content.empty?

  @notes << {:title => title, :content => content}
  @table_view.reloadData
  @title_text.stringValue = ""
  @content_text.stringValue = ""
end

Nothing too tricky here - we're validating the inputs first of all, then adding a note to our @notes array as a simple hash with :title and :content keys. We'll reload the table view so that it updates to show the new note, and then we'll clear out the text fields ready for the next note. All that remains now is to implement a couple of methods we need to handle the table view itself - specifically, we're going to be making sure we conform to the base necessities required for the NSTableViewDataSource and NSTableViewDelegate protocols. For the former, we need to add the following very simple method:

def numberOfRowsInTableView(table_view)
  @notes.length
end

This just ensures that we're telling the table view how many rows we have based on our notes in our @notes array. The next method we need, and final bit of code we have to add to see all of this working, is a little bit more involved:

def tableView(table_view, viewForTableColumn: column,   row: row)
  result = table_view.makeViewWithIdentifier(column.identifier, owner: self)
  if result.nil?
    result = NSTextField.alloc.initWithFrame([[0, 0],   [column.width, 0]])
    result.identifier = column.identifier
    result.editable = false
  end
  result.stringValue =   @notes[row][column.identifier.to_sym]
  result
end

This is called for each column, in each row - i.e. for every one of our configured columns in our layout (we have two, title and content), and then for however many rows we have based on the previous method we defined that looks at the length of the @notes array. We're calling makeViewWithIdentifier to see if we already have a view we can use in the pool, utilizing the built-in table view recycling to avoid creating too many controls. This is important, especially if you're building an app that might ultimately have hundreds or thousands of rows perhaps, and/or a lot of columns.

If we can't find a view to use, we'll set one up. We don't need to worry about any frame dimensions besides the width which we can ascertain from the configured column width, and likewise we set the identifier to match the column too. In this case we don't want the text fields editable. Lastly, regardless of how we got our view, the final thing to do is to set the string value contents of the cell - we're effectively indexing into our @notes array by both the row (to find the right note), and then using the column identifier to find the right key in our note hash to get the contents. We just have to return our control, and we're done!

If you fire up the app now, you'll see that we have the form, and an empty table. Entering a note without filling in both fields doesn't save, and filling in both fields and hitting save then clears the fields, and adds the note to our table below. You can keep adding as many notes as you'd like!

NSTableView displaying notes, with form fields for adding a note above

This is a fairly simple table view implementation, and of course you can customize an awful lot about the table view to make it more useful, to make it look nicer, and to make it better suit your purposes.

If you quit the app and restart it though, you'll see all of our precious notes have disappeared. That's where Core Data would come in handy!

You can see the code for this section here.

Comments

Dragging and dropping into your RubyMotion Mac OS X app May 20 2015

In this tutorial we're going to look at how to setup your app to receive drag and drop events - so that you can receive data simply by the user dragging something into a predefined area of your application user interface.

This can be quite useful if your app needs a file input, requires a URL, or expects some text input, amongst other things. We'll look at how to setup an area to accept the drag and drop, and how to receive and process data, separating files, from URLs, from plain old text. We'll also look at how to update your user interface to feedback to the user the status of the drag and drop operation.

Setting up a space

Let's setup a demo app that we'll use for the rest of this tutorial:

motion create --template=osx DragAndDropDemo
cd DragAndDropDemo

Now before we do anything in AppDelegate, let's setup a new view type, inheriting from NSImageView:

class DragAndDropView < NSImageView
end

This is what we'll use as our area to receive drag and drops, and we'll be building it out like a little control we can re-use and drop wherever it is needed. For our purposes, we'll just make sure it sits in the center of our default app window created as part of the RubyMotion OS X template. Add our drag and drop view setup to the end of applicationDidFinishLaunching:

@drag_and_drop = DragAndDropView.alloc.initWithFrame(CGRectZero)
self.set_drag_and_drop_frame
@mainWindow.contentView.addSubview(@drag_and_drop)

This sets up the view, and adds it to the window. As we've done in previous tutorials, we'll handle resizing by managing the view frame in a separate method (set_drag_and_drop_frame) we can call from both the initial setup, and from the window resize handler. Speaking of that, let's go ahead and make sure the AppDelegate is acting as the delegate for our @mainWindow by adding this to the bottom of the buildWindow method:

@mainWindow.delegate = self

Then we'll add the windowDidResize method to call the frame update too:

def windowDidResize(sender)
  self.set_drag_and_drop_frame
end

Lastly, we need our set_drag_and_drop_frame method to initialize the control frame:

def set_drag_and_drop_frame
  window_size = @mainWindow.frame.size
  drag_and_drop_size = [200, 200]
  @drag_and_drop.frame = [
    [
      (window_size.width / 2.0) - (drag_and_drop_size[0] / 2.0),
      (window_size.height / 2.0) - (drag_and_drop_size[1] / 2.0)
    ],
    drag_and_drop_size
  ]
end

So now we've got our view setup, we can run the app - our view doesn't display or do anything, so it won't look any different yet!

[code]

Setting up for user feedback

Before we implement our actual dragging functionality, we'll add some custom code to provide better feedback to the user when something is being dragged in to the app. Our DragAndDropView is a subclass of the NSImageView, and we'll want to keep the image rendering capability (as we'll be using that later), but for now we can override the drawing of the control to provide a highlight stroke effect, and we'll toggle that on and off as needed to indicate a drag in progress.

First things first, let's keep a variable as to whether we should be rendering the highlight or not, and provide a helper method for turning the highlight on, and one for turning the highlight off:

def highlight!
  @highlight = true
  self.needsDisplay = true
end

def unhighlight!
  @highlight = false
  self.needsDisplay = true
end

After we set the variable, in each case we trigger a re-render by setting needsDisplay to true.

Then we'll override drawRect to actually render the highlight:

def drawRect(frame)
  super(frame)

  if @highlight
    NSColor.grayColor.set
    NSBezierPath.setDefaultLineWidth(5)
    NSBezierPath.strokeRect(frame)
  end
end

As we can see here, we're calling super to keep the existing NSImageView render functionality, but in addition to that, if the highlight functionality is enabled, we're drawing a rectangle stroke effect in gray around the control itself.

Running the app right now won't show anything different yet - we still need to handle drag operations!

Handling inputs

Now we need to customize our view so it indicates that it can act as a receiver for dragging, this is done with the NSDraggingDestination protocol. The nice thing about using RubyMotion is that we don't need to explicitly declare protocols we want to implement in our classes, we just need to make sure we implement the required methods. In this case, it means adding four methods to our DragAndDropView control - draggingEntered, draggingExited, prepareForDragOperation and performDragOperation. Let's take a look at each one in turn to figure out what they are doing, and how we can use them to implement the functionality we want, which to begin with will just be supporting the dragging of files into our app:

def draggingEntered(info)
  self.highlight!
  NSDragOperationCopy
end

draggingEntered is fairly self explanatory - this fires when we drag something over the top of our control. In this case, we do two things - firstly we highlight our control to give the user some feedback that they are dragging into the right area, and secondly we need to return an indicator of what we can do with the data being given to us. In our case, we just want to display and acknowledge that we have it, and not have any effect on the original source of the data, and so we specify NSDragOperationCopy. You can inspect the info which is an object of type NSDraggingInfo, and validate it to decide what kind of operation (if any) you want to allow for it. If you want to be able to modify the source through the dragging action, there are other options such as NSDragOperationMove and NSDragOperationDelete. If you don't want to allow the drag, you can return NSDragOperationNone instead.

def draggingExited(info)
  self.unhighlight!
end

This too is fairly straightforward - draggingExited fires when the drag leaves our control area, so we'll unhighlight our control. A user might be dragging the object elsewhere, or have decided against the drag before releasing it.

def prepareForDragOperation(info)
  self.unhighlight!
end

prepareForDragOperation fires when the drag is released and is going to happen, so we can perform any preparatory steps. In this case, we need to unhighlight the control as the dragging itself is over, and it hasn't left the control area to be able to fire draggingExited, so we can do that now.

def performDragOperation(info)
  if info.draggingSource != self
    if NSImage.canInitWithPasteboard(info.draggingPasteboard)
      image = NSImage.alloc.initWithPasteboard(info.draggingPasteboard)
    else
      image = NSImage.imageNamed("file_icon")
    end
    self.setImage(image)
  end
end

This is the meat of the actual drag operation - performDragOperation is where we can action and process the data. It's worth noting that setting the image like this isn't strictly necessary - if you implement the first three methods, but not performDragOperation, and test by dragging an image file over to the control, you'll see it still works. It's basically setting the image of the control to the incoming file by default. However, we want to ensure that other files are received, and that in those cases we display a file icon to let the user know we've received them, even if we can't display them. If you try that without this method, you'll see that it really only works for images. We also want to extend the functionality later on to do a whole lot more!

So we're ensuring first of all that the source of the dragging operation isn't ourselves (i.e. dragging the current image from the control), and then we see if it's an image or not. If it is, we'll display it, and if not, we'll display the file icon instead.

Make sure to copy the file_icon.png from the code below into your resources folder for your app.

Fire it up and try dragging across files - images, and other types, and see how it reacts!

Initial drag receiver setup

[code]

Processing files

Now, you may have noticed that in the above, besides setting the image on the imageview if the dragged in file is an image, we're not actually doing anything with the incoming data. So we'll add a mechanism to our DragDropView that'll bubble up some events we can hook into from our AppDelegate to handle the incoming data. But before we do, let's setup some additional UI that we'll use when handling those events, to feed back to the user.

We'll add a label below our drag and drop field, which will serve two purposes - initially, to guide the user to dragging a file to the area above the label, and also to update with information about the data dragged in.

At the end of our applicationDidFinishLaunching, add the following:

@label = NSTextField.alloc.initWithFrame(CGRectZero)
@label.bezeled = false
@label.drawsBackground = false
@label.editable = false
@label.selectable = false
@label.alignment = NSCenterTextAlignment
@label.stringValue = "Drag a file above"
self.set_label_frame
@mainWindow.contentView.addSubview @label

As we've seen before, this uses the NSTextField control, and tweaks a few settings to provide a read-only label for us to use.

We then need to add our set_label_frame method to setup the frame itself:

def set_label_frame
  window_size = @mainWindow.frame.size
  label_size = [200, 50]
  @label.frame = [
    [(window_size.width / 2.0) - (label_size[0] / 2.0), @drag_and_drop.frame.origin.y - label_size[1]],
    label_size
  ]
end

Then, we'll add a call to set_label_frame to our resize handler windowDidResize:

def windowDidResize(sender)
  self.set_drag_and_drop_frame
  self.set_label_frame
end

Now if you fire up the app, you'll see the label sat underneath our drag and drop control.

Label

[code]

Let's extend our DragDropView to allow a delegate to be specified, and to raise an event on the delegate when we receive data. First of all, we need to add an attribute accessor to be able to specify the delegate class on the control:

class DragAndDropView < NSImageView
  attr_accessor :delegate

Next up, we'll add a method that'll be used to call methods on our delegate, that'll provide the necessary checks that we need. Add the following to our DragDropView as well:

def send_delegate_event(name, arg)
  return if self.delegate.nil?
  return unless self.delegate.respond_to?(name.to_sym)

  self.delegate.send(name.to_sym, arg)
end

This only sends the call to the specified delegate method if the delegate itself is set, and if an appropriate method is defined on the delegate to handle our event. We're defining things like this because later on we're going to add additional events for different types of data that we can receive, and ultimately we might want our AppDelegate to only respond to certain events, and not to error if we don't answer the call for other events. This is similar to how delegates work for built-in controls and events - we can set AppDelegate as the delegate for a text field for example, and define methods to handle events such as text field editing ending - but if we don't set the delegate, or don't define the method to handle the event, we don't end up with an error or a crash.

Now all that remains on the control side of things is to hook into the performDragOperation, extract the useful information and bubble that up as a delegate method call. In that method, and below our call to setImage, we'll add the following:

if info.draggingPasteboard.types.include?('NSFilenamesPboardType')
  files = info.draggingPasteboard.propertyListForType('NSFilenamesPboardType')
  self.send_delegate_event(:drag_received_for_file_paths, files)
end

So here we're validating what type of data we have and making sure we've received a drag that contains one or more filenames specified. As we'll see later on, we'll be adding to this to check for differing types of data, and thus extracting the data and raising events differently for each, allowing us to respond to different data sources in separate ways. In this case, if it is a pasteboard containing NSFilenamesPboardType, we can then load up the property list supplied for that pasteboard type, which then contains an array of the files dragged in, which is what we expose in our event call, named drag_received_for_file_paths.

Back in our AppDelegate, we now just need to set ourselves as the delegate for our drag and drop control, so where we define the @drag_and_drop instance, add:

@drag_and_drop.delegate = self

Then, we'll implement a method called drag_received_for_file_paths:

def drag_received_for_file_paths(paths)
  @label.stringValue = "Received: #{paths.join(',')}"
end

This just updates the label to list the file(s) that we've received in the drag operation. Try it out - drag one file, or a few files, and see it in action!

Handling files

[code]

Handling text

Let's extend our DragAndDropView now to handle text dragged in too. If you fire it up and try dragging across some text from a text editor, or selected text from a browser, you'll see that it doesn't respond to the drag. None of our handling fires, and the data sort of "pings back", indicating it's not being received. This is because by default, the NSImageView we're inheriting from for our control responds to certain drag types (i.e. files), but we have to tell it we want it to respond to additional types. Add the following method to our DragAndDropView class:

def initWithFrame(frame)
  super(frame)

  self.registerForDraggedTypes([NSStringPboardType])

  self
end

Here we're overriding the initialization method, calling the original one first, and then taking the opportunity to register an additional dragged type - NSStringPboardType. This will allow us to receive text. Fire up the app now, and you'll see dragging across some text works, in as much as the control is highlighted when we drag it across (as it is when we're dragging files), and also if we drop it, we don't see any errors in our console log. The data itself though isn't processed in any way, so let's do that by raising a new delegate event we can handle. In our performDragOperation, add the following to the end (but inside the outermost if statement):

text = info.draggingPasteboard.stringForType(NSPasteboardTypeString)
self.send_delegate_event(:drag_received_for_text, text) unless text.nil?

We're using stringForType and passing in NSPasteboardTypeString to represent our request to see if there is any text in the dragging pasteboard. If there is, we publish that up to the delegate with a new event, drag_received_for_text.

Finally, as before, we just need to add our delegate event handler to AppDelegate:

def drag_received_for_text(text)
  @label.stringValue = "Received: #{text}"
end

Now if we fire up the app, and drag across some text, we'll see it listed in the label. File dragging still works also, and any new drag just replaces the contents of the old one.

Handling text

[code]

Handling URLs

The last data type we'll add in for this demo is dragging in URLs, for example dragging in a URL from a browser address bar. Again, as with the text, we need to register a new dragged type for this to work - NSURLPboardType. So we'll update our call to registerForDraggedTypes to look like this:

self.registerForDraggedTypes([NSStringPboardType, NSURLPboardType])

Next up, in performDragOperation, below the text handling code we just added, add the following:

url = NSURL.URLFromPasteboard(info.draggingPasteboard)
self.send_delegate_event(:drag_received_for_url, url.absoluteString) unless url.nil?

As with the text, this is fairly straightforward - we instantiate a URL from the pasteboard, and assuming we got one, we bubble that up to a new event drag_received_for_url.

And then the final step is to implement that event handler in AppDelegate:

def drag_received_for_url(url)
  @label.stringValue = "Received: #{url}"
end

Really simple again, we're just responding to it and displaying the URL in the label. This means that by and large, our three event handlers are roughly the same, but in a real application you'd most likely go on to perform something slightly different for each data type, which is why we've structured it so that they are handled, raised and trapped as separate events.

Handling URLs

[code]

If you run the app now, you'll see URL drags from the browser address bar work into our app now. Interestingly though, if you test file drags, you'll see the URLs that are being displayed aren't the original URLs, it's an odd looking file:///.file/id=? URL. This is because the code we added for the URL handling will also work on the file drag operations too - effectively, both event types, drag_received_for_file_paths and drag_received_for_url will be raised, with drag_received_for_url handled last, and thus overwriting the label display with the funky looking URL.

Let's switch out our URL handling for the following instead:

url = info.draggingPasteboard.propertyListForType('public.url')
self.send_delegate_event(:drag_received_for_url, url) unless url.nil?

[code]

Now if you run and test it, you'll see that files display the proper path, while the URL dragging also works. It seems therefore that consulting the 'public.url' property on the pasteboard is a more accurate way to go and differentiate between those two types of dragging. When looking at the data for a specific type of drag operation, it's worth either assigning the info var passed to us in performDragOperation to a variable that you can access and play with on the console while testing, or to log out the available types at least with a statement like this:

NSLog("TYPES: #{info.draggingPasteboard.types.join(', ')}")

That will show you what types are available, and inspecting those further with propertyListForType will let you see what data is provided. It also seems that the text handling can read the URL as the string contents of the pasteboard, so you might be seeing both drag_received_for_url and drag_received_for_text fire with the URL passed as an argument - we can tidy up our handling still further to ensure we're only ever raising one event. Below our setImage call in performDragOperation, replace what we have with the following:

if info.draggingPasteboard.types.include?('NSFilenamesPboardType')
  files = info.draggingPasteboard.propertyListForType('NSFilenamesPboardType')
  self.send_delegate_event(:drag_received_for_file_paths, files)
elsif info.draggingPasteboard.types.include?('public.url')
  url = info.draggingPasteboard.propertyListForType('public.url')
  self.send_delegate_event(:drag_received_for_url, url) unless url.nil?
else
  text = info.draggingPasteboard.stringForType(NSPasteboardTypeString)
  self.send_delegate_event(:drag_received_for_text, text) unless text.nil?
end

This now just means we're matching in a sort of priority order - first filenames, then the URL, then finally falling back on the string contents of the pasteboard. As a result of daisy chaining the conditional statements though, once we have a match, no other processing happens, which results in a more reliable result for the consumer of these events, in this case, our AppDelegate.

[code]

More detailed URL handling

One thing that I spotted only when investigating the available types for a URL was that there was an additional type that included not just the URL, but the title of the webpage from the browser too! Let's add that in as an additional event, as that could be quite useful. First of all, as this would be an event with two arguments (URL and title), let's update our send_delegate_event helper method to support that:

def send_delegate_event(name, *args)
  return if self.delegate.nil?
  return unless self.delegate.respond_to?(name.to_sym)

  self.delegate.send(name, *args)
end

We've changed the arg to instead be *args, which acts as an argument list, that we're then passing straight through when we call the delegate method. We can now have methods that have 1 argument, 2 arguments, or 10 arguments!

Now let's add our extended URL handling - we're going to add this to our daisy chained conditionals, above the existing URL support which we'll leave in place in case there are any URL dragging operations that don't provide this additional data (it will most likely depend on which browser or app you're dragging the URL from):

elsif info.draggingPasteboard.types.include?('WebURLsWithTitlesPboardType')
  url, title = info.draggingPasteboard.propertyListForType('WebURLsWithTitlesPboardType').flatten
  self.send_delegate_event(:drag_received_for_url_and_title, url, title)

As you can see here, we're checking for a specific new type, WebURLsWithTitlesPboardType, and if that's available, it'll come back as an array in this format:

[["url", "title"]]

Therefore we flatten it to get just a single array, and extract our URL and title, which we bubble up with a new delegate event, drag_received_for_url_and_title, and pass both arguments. To handle this now, we just need to implement the handler on our AppDelegate:

def drag_received_for_url_and_title(url, title)
  @label.stringValue = "Received: #{url}"
  @mainWindow.title = title
end

Here we're once again putting the URL in the label to feed back to the user, but to make things a bit more interesting, we're setting our actual app window title to the title we receive from the URL web page drag. If you test that now, you'll see that in fact the window title changes to reflect the page that was dragged in!

[code]

Improving the visual feedback

The last thing we'll look at in our demo app here is to improve the user visual feedback, so that the icon shown on the image view changes for different types of drag operation - right now, an image is displayed if it's an image file, and anything else results in the file icon. Let's add a text icon and a URL icon to differentiate between those drag operations also. It'll take a bit of tweaking of our performDragOperation method, so here is how it should look in full to make this happen:

def performDragOperation(info)
  if info.draggingSource != self
    image = NSImage.alloc.initWithPasteboard(info.draggingPasteboard)
      if NSImage.canInitWithPasteboard(info.draggingPasteboard)

    if info.draggingPasteboard.types.include?('NSFilenamesPboardType')
      files = info.draggingPasteboard.propertyListForType('NSFilenamesPboardType')
      self.send_delegate_event(:drag_received_for_file_paths, files)
    elsif info.draggingPasteboard.types.include?('WebURLsWithTitlesPboardType')
      url, title = info.draggingPasteboard.propertyListForType('WebURLsWithTitlesPboardType').flatten
      self.send_delegate_event(:drag_received_for_url_and_title, url, title)
      image = NSImage.imageNamed("url_icon")
    elsif info.draggingPasteboard.types.include?('public.url')
      url = info.draggingPasteboard.propertyListForType('public.url')
      self.send_delegate_event(:drag_received_for_url, url) unless url.nil?
      image = NSImage.imageNamed("url_icon")
    else
      text = info.draggingPasteboard.stringForType(NSPasteboardTypeString)
      self.send_delegate_event(:drag_received_for_text, text) unless text.nil?
      image = NSImage.imageNamed("text_icon")
    end

    image ||= NSImage.imageNamed("file_icon")
    self.setImage(image)
  end
end

Breaking it down, we're setting our image to be the image itself if the pasteboard is an image file. Otherwise, inside of our daisy chained conditionals for matching different drag types, we're setting the appropriate image. For the files, we're not setting an image - it'll either already be set if it's an image file, or we'll be using the file icon as a default anyway, which we'll come back to in a second.

For the two URL handling conditionals, and the string/text handling, we're setting either the url_icon or text_icon image. Lastly, as we just mentioned, if no other image is set already, we'll use the file_icon as a default so we show something, before setting the image as our final action now in performDragOperation.

As before, copy the additional icons (url_icon.png and text_icon.png) from the code below into the resources directory in your app before running it.

If you run the app now and try dragging in various different data sources, you'll see that as well as the label (and for URLs with titles, the window title) changing, the icon will update to reflect the source too, which provides additional user feedback.

Better icon for handling text

Better icon for handling URLs

[code]

Wrapping up

So now we have a demo app that can receive multiple data types dragged over to it, providing feedback to the user that it can receive that drag, and then updating further when it's received and processed the data. Additionally, we've done this with a re-usable drag and drop image view control, that handles the processing of the drag operations, provides visual feedback by changing the icon shown (or showing an image file), and bubbles up various events so that a consumer app can receive more data about drag events and act accordingly. From here you could build an app that took files and uploaded them, scanned text to provide useful analysis, or altered images in a common way for repeatable tasks!

If you've enjoyed the content here, remember that early access orders for my forthcoming book, Building Mac OS X apps with RubyMotion, open up on June 8th - make sure you're subscribed to the mailing list so you can get a launch discount! Any comments or questions, please drop them below, or tweet me @ejdraper.

Comments

User specified custom key combination for a global hotkey May 13 2015

Over a year ago, I covered global hotkeys in a tutorial on this very blog. However, handling a global hotkey combo is only half of the story - I'm sure in most apps you've seen that provide that kind of functionality, the app also allows the user to override the default, and specify their own key combination to use instead. How do they do it? We're going to build on the app we already created in that tutorial (the code is here) to find out.

DDHotKey, revisited

The library we used in the last tutorial on global hotkeys, that provided a nice and neat wrapper around the old Carbon APIs for hotkeys, also provides a ready-to-use UI component for user input of hotkeys! The only thing we need to figure out is how we integrate with it, and how we store the users chosen key combination for use between app sessions. Let's start with the UI.

We're going to add the DDHotKeyTextField to the window, so at the end of our buildWindow method, we'll set it up like this:

size = @mainWindow.frame.size
field_size = [150, 30]
@hot_key_field = DDHotKeyTextField.alloc.initWithFrame(
  [
    [(size.width / 2.0) - (field_size[0] / 2.0), (size.height / 2.0) - (field_size[1] / 2.0)],
    field_size
  ])
@hot_key_field.delegate = self

@mainWindow.contentView.addSubview(@hot_key_field)

Nothing too complex there - we're centering the field in our window, setting the AppDelegate to be the delegate for the field, and adding it to our window.

Next up, we need a method to register our hotkeys. This will serve two purposes - it's what we'll call when we detect a new hotkey set using our DDHotKeyTextField, and we'll also switch the initial hotkey registration for the app to use it too, for consistency:

def registerHotKey(keyCode, modifierFlags) center = DDHotKeyCenter.sharedHotKeyCenter center.unregisterAllHotKeys center.registerHotKeyWithKeyCode( keyCode, modifierFlags: modifierFlags, target: self, action: 'handleHotkey:', object: nil) end

This should be fairly simple to follow based on how we were registering hotkeys before - basically we're passing in the key code and modifier flags that describes the hotkey combo; we're grabbing the hotkey center, this time firstly unregistering all existing hotkeys (remember this will be called when changing the hotkey so we need to remove any existing combo), and then registering the hotkey pointing to our handle hotkey method, handleHotKey, for when it is triggered.

Let's change the initial app setup to use this method to make sure it's working. In applicationDidFinishLaunching, we'll replace our registerHotKeyWithKeyCode call to the DDHotKeyCenter with a call to our new registerHotKey method:

@hot_key_field.hotKey = self.registerHotKey(KVK_ANSI_H, (NSCommandKeyMask | NSAlternateKeyMask))

Then we need to hook into when the DDHotKeyTextField is done being edited, so we can re-register the hotkey. We've already set the delegate for the field to the AppDelegate, so now all we need to do is define the following method:

def controlTextDidEndEditing(notification)
  self.registerHotKey(@hot_key_field.hotKey.keyCode, @hot_key_field.hotKey.modifierFlags) if @hot_key_field.hotKey
end

This will get called when editing is finished on that field (i.e. when enter is pressed), and it simply calls registerHotKey with the current key code and modifier flags for the field hotkey, so long as it exists.

If you fire up the app now, you should see that a default hotkey combo is set, the field allows that to be changed, and when it is changed, the app responds as expected on that new hotkey, instead of the old one.

Hotkey field

[code]

Persistence

You'll also notice though that shutting down the app and firing it back up restores the default combo, and any user customization is lost. We need to save the key code and modifier flags for any hotkey selected, so that we can use that when the app is restarted, and so if the user has changed it, it stays changed. We're going to use NSUserDefaults to store the key code and modifier flags. First things first then, when a hotkey is registered, let's store the values. Amend the top of registerHotKey to the following:

def registerHotKey(keyCode, modifierFlags)
  NSUserDefaults.standardUserDefaults['keyCode'] = keyCode
  NSUserDefaults.standardUserDefaults['modifierFlags'] = modifierFlags

The key code and modifier flags is all we need to describe the hotkey combo, so we'll stash those. Next up, let's add some helper methods for retrieving those values - and we'll move our defaults into those methods too, for the first run of the app where no values will be set:

def userKeyCode
  NSUserDefaults.standardUserDefaults['keyCode'] || KVK_ANSI_H
end

def userModifierFlags
  NSUserDefaults.standardUserDefaults['modifierFlags'] || (NSCommandKeyMask | NSAlternateKeyMask)
end

So we're looking up our persisted values, falling back to the same defaults for the key code and modifier flags that we were using before. Now, in applicationDidFinishLaunching, we can put into place the final piece of the puzzle. Instead of using our default combo there on every app startup, we'll refer to these helper methods:

@hot_key_field.hotKey = self.registerHotKey(self.userKeyCode, self.userModifierFlags)

This means that when the app is run for the first time, it'll fall back on the defaults provided within those helper methods still, but once values are persisted and saved, it'll use those instead.

Try it out now, fire up the app, change the key combo from cmd+alt+h to something else, press enter, restart the app, and then test out your new custom key combo for hiding and showing the app, persisted between app sessions. Power to the users!

[code]

Comments

Previewing video and audio when capturing on Mac OS X with RubyMotion May 7 2015

In the last article we covered how to capture video and audio from attached devices on Mac OS X using RubyMotion. In this article we're going to extend our basic AVCapture app to display a preview of the video being captured, and audio levels, so that capturing is more responsive and you can be sure you're recording what you want to record.

Previewing video

First thing first, we'll need to make room for our preview. Let's move our button to start and stop recording to the bottom left of the window - we'll put the video preview above it, and it'll leave room for our audio levels meter to the right of the button. We'll add a constant for the button size now so we can refer to it elsewhere, so add this just inside the AppDelegate class declaration:

class AppDelegate
  BUTTON_SIZE = [150, 30]

Then change set_button_frame to the following:

def set_button_frame
  @button.frame = [[0, 0], BUTTON_SIZE]
end

With the button out of the way, we can then setup the video preview. First of all, we need a view to display it in, so in our buildWindow setup method, at the bottom we'll add the following:

bounds = @mainWindow.contentView.bounds
bounds.size.height -= BUTTON_SIZE.last
bounds.origin.y += BUTTON_SIZE.last
@view = NSView.alloc.initWithFrame(bounds)
layer = CALayer.layer
@view.setLayer(layer)
@view.setWantsLayer(true)
@mainWindow.contentView.addSubview(@view)   end

We're using the BUTTON_SIZE constant to work out the rest of the window area available after the button, and then we're creating a blank view with that space. We need to setup a default blank layer using CALayer.layer, and we need to set setWantsLayer to true to be able to be able to add a sublayer which we'll be doing next from within didStartRunning:

def didStartRunning
  @video_preview = AVCaptureVideoPreviewLayer.alloc.initWithSession(@session)
  @video_preview.frame = @view.bounds
  @view.layer.addSublayer(@video_preview)

  url = NSURL.alloc.initWithString("file:///Users/#{NSUserName()}/Desktop/temp#{Time.now.to_i}.mp4")
  @output.startRecordingToOutputFileURL(url, recordingDelegate: self)
end

Before we start our recording, we're using the AVCaptureVideoPreviewLayer class, configured against our @session, to be able to create a layer; then we're setting the bounds to match the view, and then we're adding it as a sublayer. That's it!

Run the app now, and when you start recording the captured output appears in the window above our button, and it disappears when we stop our capture session. Pretty cool huh?

Captured video preview

[code]

Coping with window resizes

We already handled window resizes for our centered button in the code, based upon our centered button code example here - however now our button is in the bottom left (and stays there), that is no longer the problem. Instead, our video preview doesn't properly size up or down on resize when already active. So let's handle the resize event in the same way, and re-initialize the view holding our video preview layer any time a resize happens to properly update the preview. First things first, let's add a method to handle the setup and updating of our video preview frame, which we can now call both on initial capture start, as well as on resize, to keep the preview size in sync with the window:

def update_video_preview
  if @view
    @video_preview.removeFromSuperlayer if @video_preview
    @view.removeFromSuperview
  end

  bounds = @mainWindow.contentView.bounds
  bounds.size.height -= BUTTON_SIZE.last
  bounds.origin.y += BUTTON_SIZE.last
  @view = NSView.alloc.initWithFrame(bounds)
  layer = CALayer.layer
  @view.setLayer(layer)
  @view.setWantsLayer(true)
  @mainWindow.contentView.addSubview(@view)

  @video_preview.frame = @view.bounds
  @view.layer.addSublayer(@video_preview)
end

As you can see, we're coping with an existing @view and @video_preview layer, removing them from their parent view/layer as appropriate, before using the window bounds (minus the button size) to calculate the room left for the preview - the same as we were doing before. We then add the view, set the video preview layer bounds based on the view we've setup, and add the video preview layer as a sublayer. The only thing we're not doing here is creating the actual video preview layer - this we will do only when the capture session starts, because if we did it in update_video_preview, getting called on each resize, we'd find that recreating new video preview layers against the session would actually trigger it to stop recording output for some reason. Seeing as we don't need to reinitialize that anyway, we just need to remove it from the old view and add it to the new one, then it doesn't matter too much.

So now we need to reference this when the capture session starts, so that our code works as it did before - the first part of didStartRunning should now look like this:

def didStartRunning
  @video_preview = AVCaptureVideoPreviewLayer.alloc.initWithSession(@session)
  self.update_video_preview

We'll create the video preview layer as before, and then we'll call our new method for updating the video preview, in this case initialising the view and adding the sublayer to get the output showing. Lastly, we just need to hook things in on resize, as so within windowDidResize:

def windowDidResize(notification)
  self.set_button_frame
  self.update_video_preview if @is_running
end

We check to make sure @is_running is equal to true, to be sure that it's worth bothering with a video preview.

Now if you run the app, you'll see that resizing the window resizes the video output preview in real-time - pretty neat!

One thing we now need to fix though is that with the way this is setup, you'll notice trying to record more than one video during one run of the app doesn't work. The first one records fine, and from then on, it doesn't begin recording. This is because re-initializing the video preview layer in this way each session (while all of the rest of the session and recording setup is unchanged) causes an error (AVErrorSessionConfigurationChanged specifically), somewhat similar to how we didn't want it re-initialised on each window resize or else it'd stop recording. So we'll change didStartRunning so it only sets up @video_preview when it doesn't already exist:

def didStartRunning
  @video_preview ||= AVCaptureVideoPreviewLayer.alloc.initWithSession(@session)

Now when we start multiple recordings in one run of the app, none of the AV related configuration or setup changes, and so we're free to just start and stop the session and create as many recordings as we want.

When you do get an error that seems to prevent recording (in our app it'll just keep going back to Start and shutting the webcam back off a few seconds after pressing Start), you can usually get to the bottom of it from within didFinishRecordingToOutputFileAtURL. That will still get called, but you can now use the error parameter to see what's going on. The message isn't always very useful, but the code property will provide a value you can check against the error codes here, which along with the description of each error further down that document, should give you enough to go on. Those are also very useful of course because in a production ready app, you'd want to cope with a lot of those errors and feed them back to the user, for things such as being out of disk space etc.

[code]

Tapping into audio levels

Next up, let's use the space to the right of the start/stop button to show an audio level meter. Before we get in to the user interface though, first we'll look at how to tap into the necessary audio output to poll for audio peak and average levels.

We need to add an audio output, so in applicationDidFinishLaunching, below where we setup our movie file output, let's add this:

@audio_output = AVCaptureAudioDataOutput.alloc.init
@session.addOutput(@audio_output) if @session.canAddOutput(@audio_output)

This creates an audio data output and adds it to the session, ready for us to use. The rate of change for the average power and peak hold levels on audio channels though is so quick that there aren't any delegate methods or notifications we can hook into to watch for changes, so instead we just need to poll for it - the resolution is up to you and the app you're building, but in our example we'll be polling 10 times a second. We'll do that with a scheduled interval, so add this to the bottom of applicationDidFinishLaunching:

NSTimer.scheduledTimerWithTimeInterval(0.1,
  target: self,
  selector: 'checkAudioLevels',
  userInfo: nil,
  repeats: true)

This repeats endlessly while the app is running, and runs every 0.1s. Now we just need to implement checkAudioLevels and inspect the audio output:

def checkAudioLevels
  return unless @is_running
  sum = 0
  @audio_output.connections.first.audioChannels.each_with_index do |channel, index|
    NSLog "CHANNEL[#{index}]: avg: #{channel.averagePowerLevel}, peak: #{channel.peakHoldLevel}"
    sum += (channel.averagePowerLevel + channel.peakHoldLevel) / 2.0
  end
  avg = sum / @audio_output.connections.first.audioChannels.count
  NSLog "AVERAGE AVERAGE: #{avg}"
end

Let's run through what this is doing - firstly we make sure we're running a capture before we do anything, and if it is, we loop through the channels on the audio outputs connection (it can have only one connection), and we're logging out the average power and peak hold level. We're also adding the average of those two values for each channel to a running total, and then creating an average single value based on both average power and peak hold, for all available audio channels, and logging that out too. If you run the app now, then start the session, you'll notice that any noises make a significant change to the numbers being shown on the console.

Logging audio levels to the console

[code]

Displaying audio levels visually

So we have the data, but we need a way of displaying that to the user so that they can see that audio levels are as expected. We're going to use a simple custom meter control that I created to display the levels - the idea being that it's a bar, with color thresholds such that normal volume shows green, louder shows yellow, and extremely noisy shows as red. The code for the control itself is available here, and you'll be able to see a bit more about how it works in a chapter in my forthcoming book.

First things first then, let's add the gem we need to our Gemfile:

source 'https://rubygems.org'

gem 'rake'
# Add your dependencies here:

gem 'motion-meter'

Then in our applicationDidFinishLaunching, above the NSNotificationCenter and NSTimer code, we'll add our new control as follows:

@audio_level = Motion::Meter::ThresholdMeter.alloc.initWithFrame(CGRectZero)
@audio_level.add_threshold(-20, -5, NSColor.greenColor)
@audio_level.add_threshold(-5, 3, NSColor.yellowColor)
@audio_level.add_threshold(3, 10, NSColor.redColor)
@audio_level.min_value = -20
@audio_level.max_value = 10
self.set_audio_level_frame
@mainWindow.contentView.addSubview(@audio_level)

The meter control allows thresholds to be set, and for that we just need to define a lower and upper limit for the threshold, along with a color to use for rendering the meter if the current value is within that threshold. We also set a min and max value to prepare the control for the range of values to expect - typically our calculated average works out at around somewhere between -20 and 10 it seems from the logging, so we'll set it up accordingly.

We'll also need to add this method that is called to setup the frame too:

def set_audio_level_frame
  @audio_level.frame = [
    [BUTTON_SIZE.first, 0],
    [@mainWindow.contentView.bounds.size.width - BUTTON_SIZE.first, BUTTON_SIZE.last]
  ]
end

And the reason that's in its own method, as with the set_button_frame, is so that we can also call it from the resize handler when the window is resized. Therefore, windowDidResize becomes:

def windowDidResize(notification)
  self.set_button_frame
  self.set_audio_level_frame
  self.update_video_preview if @is_running
end

Lastly, we need to be setting the value from within our checkAudioLevels method, so the control can use the latest value each time we check the levels - here we're using our calculated average value:

def checkAudioLevels
  return unless @is_running
  sum = 0
  @audio_output.connections.first.audioChannels.each_with_index do |channel, index|
    sum += (channel.averagePowerLevel + channel.peakHoldLevel) / 2.0
  end
  avg = sum / @audio_output.connections.first.audioChannels.count
  @audio_level.value = avg
end

You'll notice we've also lost our logging statements - now that we're representing those values visually through our meter control, they are no longer needed.

If we fire up our app, and click Start, we'll see our video preview appear, and below it, alongside our button, a meter that reacts to the input audio volume of our recording - try clapping and see it react accordingly, jumping into the yellow or red thresholds.

Displaying calculated audio level

[code]

Next steps

There is obviously a whole lot more that you could do with the AVFoundation framework, we've really just scratched the surface of what's possible here. Capturing still images, altering the configuration and presets to change the quality of the output, processing data to modify it on the fly, as well as doing more interesting things with the output such as streaming or uploading - there is a lot of potential. In my book, you'll be able to read this content and more to cover capturing audio and video in more detail, as well as a whole bunch of other OS X app building topics using RubyMotion. If you're interested in being able to build apps and utilities for OS X quickly and easily, you'll want this book! So sign up now below to be notified when early access starts very soon (subscribers will get an exclusive discount!).

Other than that, feel free to tweet me @ejdraper, or comment below with any questions or comments.

Comments

Page 1 of 31 | Next page