SlideShare a Scribd company logo
PHP ON THE DESKTOP AND PHP-GTK2
Elizabeth Marie Smith – Zend Uncon 2008
ABOUT ME
Elizabeth Marie Smith

• AKA auroraeosrose
• Have a very very common name
• Was an English Major (minor in Sociology)
• Started using PHP for a Manga/Anime fan site (still alive)
• Have been using PHP since 4 beta 1 (23-Jul-1999)
• Have been using C since Nov of 2006
• Love the color Pink
• I hate computers
• I love programming
• Thinks open source on Windows is a “gateway drug”
• Works at OmniTI (http://omniti.com)
• Am a contributor on various open source projects (including
PHP, PECL and PHP-GTK)
IS THE DESKTOP DEAD?
THE DESKTOP STILL LIVES
Reasons For Desktop Applications


• People aren’t always online
• Some applications would be bandwidth
prohibitive
• Some data is too sensitive to be passed
on the wire
• Embedded OSs don’t always work well
with websites
• Browsers are just Quirky (I’m looking at you,
IE6, IE7, Opera…)
• Current Desktop RIA tools (AIR, Xulrunner, Mozilla
Prism) are still young (and buggy)
WHY USE PHP FOR DESKTOP APPLICATIONS
PHP Advantages




• No Compiling
• Instant Changes
• No learning curve for a new language
• Plug into PHP extensions
• Easy to Use
• Fast to Write
• Use already familiar libraries and tools
WHY USE PHP FOR DESKTOP APPLICATIONS
PHP Disadvantages


• Speed
• Additional Extensions/Libraries needed
• Speed
• Distribution (phar is helpful there)
• Speed
• Security (of code – source code encoders
  might help here)
• No threading

                    See the theme?
HOW DO I WRITE DESKTOP APPS?
PHP CLI


• PHP does not care about HTML (really)
• PHP works through SAPIS (Server
  Application Programming Interface) – for
  the desktop the CLI (Command Line
  Interface) SAPI is our friend
• CLI has no headers, CLI responds to no
  requests
• You need a way to make GUI’s if you don’t
  want a console app – Just like we wrap
  other C/C++ libraries we can wrap GUI
  libraries
GUI OPTIONS WITH PHP
So What are your Options?


•        Well Established
          • PHP-GTK2
          • Winbinder

•        Works, but not Complete
          • PHP-QT (no windows support)
          • WxWidgets (only windows support)
          • Win::Gui (part of Win::API)


    Bottom line? For Cross-Platform PHP-GTK works
WTH IS GTK?
Quick Intro


• GTK was Gimp Tool Kit (long long ago)
• GTK is GUI Toolkit for Gnome (if you use
  gnome, you have it already)
• GTK has multiple backends (for native
  Windows and MacOSX apps – you don’t
  need X on Mac anymore)
• To use PHP-GTK you need PHP CLI, GTK, and
  the PHP-GTK extension
•        http://oops.opsat.net/doc/install.html - how to install
         (windows is unzip and run, mac has an installer, ubuntu has
         .debs – yes this should be in the php-gtk docs, we need
         volunteers who aren’t afraid of xml)
LET’S DO PHP-GTK
THE BASICS
What you need to know to use PHP-GTK




• Main Loop
• Signals & Callbacks
• Widgets
• OOP to the Max
• Visibility
STARTING OUR PROJECT
Create Our initial Window                                                Resulting window
<?php
class Php_Gtk_Twitter_Client extends GtkWindow {

      public function __construct() {
          parent::__construct();
          $this->set_icon($this->render_icon(
             Gtk::STOCK_ABOUT, Gtk::ICON_SIZE_DIALOG));
          $this->set_size_request(300, 500);
          $this->set_title('PHP-GTK Twitter Client');
          $this->connect_simple('destroy', array('Gtk', 'main_quit'));
      }
}
$window = new Php_Gtk_Twitter_Client;
$window->show_all();
Gtk::main();
OVERRIDING BASIC FUNCTIONALITY
Minimize to the Taskbar




• Specialty Widgets are Cool
• GtkStatusIcon – 2.10+
• Know your Hierarchy
• Stock Icons
• Callback “bubbling”
• timeouts (timers)
STOCK ITEMS
Demo from PHP-GTK
Minimize to Status Icon




                                                                                                                                    MINIMIZE TO TRAY
 <?php                                                                                                           Resulting Window
 class Php_Gtk_Twitter_Icon extends GtkStatusIcon {
      protected $alive;
      protected $lockout;
      public function __construct() {
          parent::__construct();
            $this->set_from_stock(Gtk::STOCK_ABOUT);
            $this->set_tooltip('PHP-GTK Twitter Client');
            while(Gtk::events_pending() || Gdk::events_pending()) {
                  Gtk::main_iteration_do(true);
            }
            $this->is_ready();
            return;
      }
      public function is_ready() {
          $this->alive = true;
          if($this->lockout < 5 && !$this->is_embedded()) {
                Gtk::timeout_add(750,array($this,'is_ready'));
                ++$this->lockout;
                $this->alive = false;
          } else if(!$this->is_embedded()) {
                die("Error: Unable to create Tray Icon. Please insure that your system's tray is enabled.n");
                $this->alive = false;
          }
            return;
      }
      public function activate_window($icon, $window) {
          if ($window->is_visible()) {
              $window->hide();
          } else {
              $window->deiconify();
              $window->show();
          }
      }
 }
 class Php_Gtk_Twitter_Client extends GtkWindow {
      protected $statusicon;
       public function __construct() {
            parent::__construct();
            $this->set_icon($this->render_icon(Gtk::STOCK_ABOUT, Gtk::ICON_SIZE_DIALOG));
            $this->set_size_request(300, 500);
            $this->set_title('PHP-GTK Twitter Client');
            $this->connect_simple('destroy', array('Gtk', 'main_quit'));
            $this->statusicon = new Php_Gtk_Twitter_Icon;
            $this->statusicon->connect('activate', array($this->statusicon,
                  'activate_window'), $this);
            $this->set_skip_taskbar_hint(true);
            $this->connect('window-state-event', array($this, 'minimize_to_tray'));
      }
      public function minimize_to_tray($window, $event) {
          if ($event->changed_mask == Gdk::WINDOW_STATE_ICONIFIED &&
              $event->new_window_state & Gdk::WINDOW_STATE_ICONIFIED) {
              $window->hide();
          }
          return true; //stop bubbling
      }
 }
 $window = new Php_Gtk_Twitter_Client;
 $window->show_all();
 Gtk::main();
VIEWING DATA
Data from PHP + a Treeview from GTK




• Remember this is PHP
• Treeview has a model and view
• ListStore is a single level model
• Treeviews need Columns
• Columns need Renderers
• Different types of Renderers
MY TWITTER API
HTTP Streams are GOOD

I really don’t believe in wheel inventing , especially when I have little experience with the subject. However there is an issue – every
existing twitter API uses curl – and I don’t want to depend on an extension that isn’t always installed.


        protected function process($url, $date = 0, $type = 'GET', $data = null) {
                  // add caching header
                  $this->headers[0] = 'If-Modified-Since: ' . date(DATE_RFC822, $date);

                   $options = array(
                          'http' => array(
                                 'method' => $type,
                                 'header' => $this->headers)
                          );
                   if (!is_null($data)) {
                          $options['http']['content'] = http_build_query($data);
                   }
                   $context = stream_context_create($options);
                   if ($this->username && $this->password) {
                          $base = 'http://' . urlencode($this->username) . ':' . urlencode($this->password)
                          . '@twitter.com/';
                   } else {
                          $base = 'http://twitter.com/';
                   }
                   set_error_handler(array($this,'swallow_error'));
                   $string = file_get_contents($base . $url, false, $context);
                   restore_error_handler();
                   return json_decode($string);
             }
MINIMIZE TO TRAY
   Put a Treeview in the Window – this goes in __construct                                Callbacks for showing Images and Messages
$this->temp = sys_get_temp_dir() . 'php-gtk-twitter-api-cache';                     public function show_user($column, $cell, $store, $position) {
if (!file_exists($this->temp)) {                                                                  $pic = $store->get_value($position, 1);
                                                                                                  $name = $this->temp . md5($pic);
    mkdir($this->temp, null, true);                                                               if (isset($this->pic_queue[$name])) {
}                                                                                                       return;
                                                                                                  } elseif (isset($this->pic_cached[$name])) {
                                                                                                        $store = $this->treeview->get_model();
$this->twitter = new Php_Gtk_Twitter_Api;                                                               if (is_null($store->get_value($position, 0))) {
                                                                                                               $pixbuf = GdkPixbuf::new_from_file($name . '.jpg');
// User image pixbuf, user image string, user name,                                                            $store->set($position, 0, $pixbuf);
                                                                                                               $cell->set_property('pixbuf', $pixbuf);
// user id, text, favorited, created_at, id                                                             }
  $store = new GtkListStore(GdkPixbuf::gtype, Gobject::TYPE_STRING,                                     return;
                                                                                                  }
   Gobject::TYPE_STRING, Gobject::TYPE_LONG, GObject::TYPE_STRING,                                $this->pic_queue[$name] = array('name' => $name, 'url' => $pic,
   GObject::TYPE_BOOLEAN, GObject::TYPE_STRING, Gobject::TYPE_LONG);                                    'pos' => $position, 'cell' => $cell);
                                                                                                  if (empty($this->load_images_timeout)) {
$store->set_sort_column_id(7, Gtk::SORT_DESCENDING);                                                    $this->load_images_timeout = Gtk::timeout_add(500, array($this, 'pic_queue'));
                                                                                                  }
$list = $this->twitter->get_public_timeline();                                              }
                                                                                            public function pic_queue() {
// stuff the store                                                                              $pic = array_shift($this->pic_queue);
foreach($list as $object) {                                                                     if (empty($pic)) {
      $store->append(array(null, $object->user->profile_image_url, $object->user->name,               $this->load_images_timeout = null;
                                                                                                      return true;
             $object->user->id, $object->text, $object->favorited, $object->created_at,         }
             $object->id));                                                                     if (!file_exists($pic['name'])) {
}                                                                                                     file_put_contents($pic['name'] . '.jpg', file_get_contents($pic['url']));
                                                                                                      $this->pic_cached[$pic['name']] = $pic['url'];
                                                                                                }
$this->public_timeline_timeout = Gtk::timeout_add(61000,                                        return true; // keep the timeout going
  array($this, 'update_public_timeline')); // every 60 seconds                              }
                                                                                            public function message_markup($column, $cell, $store, $position) {
$scrolled = new GtkScrolledWindow();                                                            $user = utf8_decode($store->get_value($position, 2));
                                                                                                $message = utf8_decode($store->get_value($position, 4));
$scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);                                   $time = $this->distance($store->get_value($position, 6));
$this->add($scrolled);
$this->treeview = new GtkTreeView($store);                                                        $message = htmlspecialchars_decode($message, ENT_QUOTES);
$scrolled->add($this->treeview);                                                                  $message = str_replace(array('@' . $user, '&nbsp;', '&'), array('<span foreground="#FF6633">@' .
$this->treeview->set_property('headers-visible', false);                                            $user . '</span>', ' ', '&amp;'), $message);
                                                                                                  $cell->set_property('markup', "<b>$user</b>:n$messagen<small>$time</small>");
$this->treeview->set_rules_hint(true);                                                      }

$picture_renderer = new GtkCellRendererPixbuf();                                            protected function distance($from) {
                                                                                                $minutes = round(abs(time() - strtotime($from)) / 60);
$picture_column = new GtkTreeViewColumn('Picture', $picture_renderer, 'pixbuf', 0);
$picture_column->set_cell_data_func($picture_renderer, array($this, 'show_user'));                switch(true) {
$this->treeview->append_column($picture_column);                                                      case ($minutes == 0):
                                                                                                           return 'less than 1 minute ago';
                                                                                                      case ($minutes < 1):
$message_renderer = new GtkCellRendererText();                                                             return '1 minute ago';
$message_renderer->set_property('wrap-mode', Gtk::WRAP_WORD);                                         case ($minutes <= 55):
                                                                                                           return $minutes . ' minutes ago';
$message_renderer->set_property('wrap-width', 200);                                                   case ($minutes <= 65):
$message_renderer->set_property('width', 10);                                                              return 'about 1 hour ago';
                                                                                                      case ($minutes <= 1439):
$message_column = new GtkTreeViewColumn('Message', $message_renderer);                                     return 'about ' . round((float) $minutes / 60.0) . ' hours';
                                                                                                      case ($minutes <= 2879):
$message_column->set_cell_data_func($message_renderer,                                                     return '1 day ago';
  array($this, 'message_markup'));                                                                    default:
                                                                                                           return 'about ' . round((float) $minutes / 1440) . ' days ago';
$this->treeview->append_column($message_column);                                                  }
                                                                                            }
$this->treeview->set_resize_mode(Gtk::RESIZE_IMMEDIATE);
A WORKING VIEW OF TWITTER MESSAGES
It works! Public Timeline in Action
PACKING
Putting other things in a Window




• GTK is not “pixel perfect”
• Packing is not as hard as it looks
• Containers can hold one or many
• Remember that a container
expands to the size of it’s contents
TOOLBARS AND STATUSBARS
Pack the Statusbar, Treeview, and Toolbar in a VBox                         Create Statusbar and Toolbar

$this->statusbar = new GtkStatusBar();

// Create a toolbar with login button
$tb = new GtkToolbar();
$tb->set_show_arrow(false);
$this->loginbutton = GtkToolButton::new_from_stock(Gtk::STOCK_JUMP_TO);
$this->loginbutton->set_label('Login');
$this->loginbutton->connect_simple('clicked', array($this, 'login'));
$tb->insert($this->loginbutton, -1);

// logout button
$this->logoutbutton = GtkToolButton::new_from_stock(Gtk::STOCK_CLOSE);
$this->logoutbutton->set_label('Logout');
$this->logoutbutton->connect_simple('clicked', array($this, 'logout'));
$tb->insert($this->logoutbutton, -1);
$this->logoutbutton->set_sensitive(false);


With Toolbar, Treeview, and Statusbar
$vbox = new GtkVBox();
$this->add($vbox);
$vbox->pack_start($tb, false, false);
$vbox->pack_start($scrolled);
$vbox->pack_start($this->statusbar, false, false);

Update the Header and Public Timeline using a Gtk::timeout
public function update_public_timeline() {
      $this->pic_queue = array();
      $list = $this->twitter->get_public_timeline();
      $this->statusbar->pop(1);
      $this->statusbar->push(1, 'last updated ' . date('Y-m-d H:i') .
         ' ' . count($list) . ' new tweets');
      $store = $this->treeview->get_model();
      $store->clear();
      foreach($list as $object) {
              $store->append(array(null,
                 $object->user->profile_image_url, $object->user->name,
                    $object->user->id, $object->text, $object->favorited,
                    $object->created_at, $object->id));
      }
      return true;
}
DIALOGS
Additional Windows




• Dialogs are cool
• Dialogs are usually modal, but
not always
• Dialogs can be very general or
very specific
• You can put anything inside one
LOGIN DIALOG BOX
Extends GtkDialog                                                           Login Dialog
class Php_Gtk_Twitter_Login_Dialog extends GtkDialog {
       protected $emailentry;
       protected $passwordentry;
       public function __construct($parent) {
           parent::__construct('Login to Twitter', $parent, Gtk::DIALOG_MODAL,
                 array(
                             Gtk::STOCK_OK, Gtk::RESPONSE_OK,
                             Gtk::STOCK_CANCEL, Gtk::RESPONSE_CANCEL));
           $table = new GtkTable();
           $email = new GtkLabel('Email:');
           $table->attach($email, 0, 1, 0, 1);
           $password = new GtkLabel('Password:');
           $table->attach($password, 0, 1, 1, 2);
           $this->emailentry = new GtkEntry();
           $table->attach($this->emailentry, 1, 2, 0, 1);
           $this->passwordentry = new GtkEntry();
           $table->attach($this->passwordentry, 1, 2, 1, 2);
           $this->passwordentry->set_visibility(false);
           $this->vbox->add($table);
           $this->errorlabel = new GtkLabel();
           $this->vbox->add($this->errorlabel);
           $this->show_all();
       }
        public function check_login($twitter) {
              $this->errorlabel->set_text('');
              $email = $this->emailentry->get_text();
              $password = $this->passwordentry->get_text();
              if (empty($password) || empty($password)) {
                    $this->errorlabel->set_markup(
    '<span color="red">Name and Password must be entered</span>');
                    return false;
              }
              if ($twitter->login($email, $password)) {
                    return true;
              } else {
                    $this->errorlabel->set_markup(
                '<span color="red">Authentication Error</span>');
                    return false;
              }
        }
}
LOGIN DIALOG BOX
Callbacks                                                                             After Login
public function login() {
            if (!empty($this->load_images_timeout)) {
                  Gtk::timeout_remove($this->load_images_timeout);
                  $readd = true;
            }
            Gtk::timeout_remove($this->public_timeline_timeout);
            $login = new Php_Gtk_Twitter_Login_Dialog($this);
            while($response = $login->run()) {
                  if ($response == GTK::RESPONSE_CANCEL ||
                        $response == GTK::RESPONSE_DELETE_EVENT) {
                          if (isset($readd)) {
                                $this->load_images_timeout =
                                  Gtk::timeout_add(500, array($this, 'pic_queue'));
                          }
                          $this->public_timeline_timeout =
                                 Gtk::timeout_add(61000,
                             array($this, 'update_public_timeline'));
                          $login->destroy();
                          break;
                  } elseif ($response == GTK::RESPONSE_OK) {
                          if($login->check_login($this->twitter)) {
                                $this->logoutbutton->set_sensitive(true);
                                $this->loginbutton->set_sensitive(false);
                                $login->destroy();
                                $this->public_timeline_timeout = Gtk::timeout_add(
                                         61000, array($this, 'update_timeline'));
                                $this->load_images_timeout = Gtk::timeout_add(500,
                                        array($this, 'pic_queue'));
                                $this->treeview->get_model()->clear();
                                $this->pic_queue = array();
                                $this->pic_cached = array();
                                $this->update_timeline();
                                break;
                          }
                  }
            }
      }

public function logout() {
            $this->twitter->logout();
            $this->logoutbutton->set_sensitive(false);
            $this->loginbutton->set_sensitive(true);
            $this->public_timeline_timeout = Gtk::timeout_add(61000,
                  array($this, 'update_public_timeline')); // every 60 seconds
            $this->pic_queue = array();
            $this->pic_cached = array();
            $this->update_public_timeline();
      }
DATA ENTRY
Putting in Data




• GTKEntry
• Basic Data Entry – activates on
return, can set maximum length
allowed
• Simple label for messages –
could use a dialog or other
method of informing the user
LOGIN DIALOG BOX
Packing it in                                                           Creating the Entries
$vbox = new GtkVBox();                                                              // Create an update area
$this->add($vbox);                                                                  $this->updateentry = new GtkEntry();
$vbox->pack_start($tb, false, false);                                               $this->updateentry->set_max_length(140);
$scrolled = new GtkScrolledWindow();                                                $this->updateentry->set_sensitive(false);
$scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);                       $this->updateentry->connect('activate',
$vbox->pack_start($scrolled);                                                          array($this, 'send_update'));
$vbox->pack_start(new GtkLabel('What are you doing?'), false, false);               $this->entrystatus = new GtkLabel();
$vbox->pack_start($this->updateentry, false, false);
$vbox->pack_start($this->entrystatus, false, false);
$vbox->pack_start($this->statusbar, false, false);




The Callback on Activate


public function send_update($entry) {
            if ($this->twitter->send($entry->get_text())) {
                  $this->entrystatus->set_text('Message Sent');
                  $this->update_timeline();
                  $this->updateentry->set_text('');
            } else {
                  $this->entrystatus->set_markup(‘
   <span color="red">Error Sending Message - Try Again</span>');
            }
      }
PHP-GTK TWITTER CLIENT
Completed App
COMPLETED CODE
Final Code, all 492 lines
class Php_Gtk_Twitter_Api {
                                                                             public function get_timeline() {
       protected $login = false;                                                   if ($this->login && $this->can_call()) {
       protected $username;                                                            if (empty($this->lastid)) {
       protected $password;                                                                  $data = $this->process('statuses/friends_timeline.json');
                                                                                       } else {
                                                                                             $data = $this->process('statuses/friends_timeline.json', $this->lasttime,
      protected $cached_public;                                                               'GET', array('since_id' => $this->lastid));
                                                                                       }
      protected $cached_public_timestamp = 0;                                          if ($data) {
                                                                                             $this->lastid = $data[0]->id;
                                                                                       }
      protected $lastid = 0;                                                           $this->lasttime = time();
                                                                                       return $data;
      protected $headers = array                                                   }
                                                                               }
         ('', 'X-Twitter-Client: PHP-GTK Twitter Client',
                                                                               public function send($message) {
           'X-Twitter-Client-Version: 0.1.0-dev',                                  if ($this->login && $this->can_call()) {
           'X-Twitter-Client-URL: http://elizabethmariesmith.com');                    $data = $this->process('statuses/update.json', 0, 'POST', array('status' => $message));
                                                                                       if ($data) {
                                                                                             return true;
      public function login($username, $password) {                                    }
          $this->username = $username;                                                 return false;
                                                                                   }
          $this->password = $password;                                         }
          $worked = $this->process('account/verify_credentials.json');
                                                                               protected function can_call() {
          if ($worked && $worked->authorized == true) {                            if (!$this->login) {
                $this->login = true;                                                    return false;
                                                                                   }
                return true;                                                       $worked = $this->process('account/rate_limit_status.json');
          }                                                                        return ($worked->remaining_hits > 1);
          return false;                                                        }
      }                                                                        protected function process($url, $date = 0, $type = 'GET', $data = null) {
                                                                                   // add caching header
                                                                                   $this->headers[0] = 'If-Modified-Since: ' . date(DATE_RFC822, $date);
      public function logout() {
          $this->username = null;                                                    $options = array(
                                                                                           'http' => array(
          $this->password = null;                                                                  'method' => $type,
          $this->login = false;                                                                    'header' => $this->headers)
          $this->process('account/end_session', 0, 'POST');                                );
                                                                                     if (!is_null($data)) {
      }                                                                                    $options['http']['content'] = http_build_query($data);
                                                                                     }
                                                                                     $context = stream_context_create($options);
      public function get_public_timeline() {                                        if ($this->username && $this->password) {
          if ($this->cached_public_timestamp < time()) {                                   $base = 'http://' . urlencode($this->username) . ':' . urlencode($this->password)
                                                                                           . '@twitter.com/';
               $this->cached_public =                                                } else {
                  json_decode(file_get_contents(                                           $base = 'http://twitter.com/';
                                                                                     }
                 'http://twitter.com/statuses/public_timeline.json'));               set_error_handler(array($this,'swallow_error'));
               $this->cached_public_timestamp = time() + 60;                         $string = file_get_contents($base . $url, false, $context);
                                                                                     restore_error_handler();
              // caches every 60 seconds                                             return json_decode($string);
                                                                               }
          }
          return $this->cached_public;                                         public function swallow_error($errno, $errstr) {} // this should be treated as private
      }                                                                  }
COMPLETED CODE
Final Code, all 492 lines
class Php_Gtk_Twitter_Login_Dialog extends GtkDialog {                class Php_Gtk_Twitter_Icon extends GtkStatusIcon {
       protected $emailentry;
       protected $passwordentry;                                           protected $alive;
                                                                           protected $lockout;
      public function __construct($parent) {
          parent::__construct('Login to Twitter',                          public function __construct() {
            $parent, Gtk::DIALOG_MODAL,                                        parent::__construct();
                array(Gtk::STOCK_OK, Gtk::RESPONSE_OK,
                         Gtk::STOCK_CANCEL, Gtk::RESPONSE_CANCEL));              $this->set_from_stock(Gtk::STOCK_ABOUT);
          $table = new GtkTable();
                                                                                 $this->set_tooltip('PHP-GTK Twitter Client');
          $email = new GtkLabel('Email:');
          $table->attach($email, 0, 1, 0, 1);                                    while(Gtk::events_pending() || Gdk::events_pending()) {
          $password = new GtkLabel('Password:');                                       Gtk::main_iteration_do(true);
          $table->attach($password, 0, 1, 1, 2);                                 }
          $this->emailentry = new GtkEntry();                                    $this->is_ready();
          $table->attach($this->emailentry, 1, 2, 0, 1);                         return;
          $this->passwordentry = new GtkEntry();                           }
          $table->attach($this->passwordentry, 1, 2, 1, 2);
          $this->passwordentry->set_visibility(false);                     public function is_ready() {
          $this->vbox->add($table);
                                                                               $this->alive = true;
          $this->errorlabel = new GtkLabel();
          $this->vbox->add($this->errorlabel);                                 if($this->lockout < 5 && !$this->is_embedded()) {
          $this->show_all();                                                         Gtk::timeout_add(750,array($this,'is_ready'));
      }                                                                              ++$this->lockout;
                                                                                     $this->alive = false;
      public function check_login($twitter) {                                  } else if(!$this->is_embedded()) {
           $this->errorlabel->set_text('');                                          die("Error: Unable to create Tray Icon.
           $email = $this->emailentry->get_text();                                      Please insure that your system's tray is enabled.n");
           $password = $this->passwordentry->get_text();                             $this->alive = false;
           if (empty($password) || empty($password)) {
                                                                               }
                 $this->errorlabel->set_markup(
         '<span color="red">Name and Password must be entered</span>');
                                                                                 return;
                 return false;
           }                                                               }
           if ($twitter->login($email, $password)) {
                 return true;                                              public function activate_window($icon, $window) {
           } else {                                                            if ($window->is_visible()) {
                 $this->errorlabel->set_markup(                                    $window->hide();
                    '<span color="red">Authentication Error</span>');          } else {
                 return false;                                                     $window->deiconify();
           }                                                                       $window->show();
      }                                                                        }
}                                                                          }
COMPLETED CODE
Final Code, all 492 lines
class Php_Gtk_Twitter_Client extends GtkWindow {                                                                        foreach($list as $object) {
                                                                                                                               $store->append(array(null, $object->user->profile_image_url,
     protected      $statusicon;
     protected      $twitter;                                                                                                      $object->user->name, $object->user->id,
     protected      $treeview;                                                                                                     $object->text, $object->favorited, $object->created_at,
     protected      $statusbar;
                                                                                                                                      $object->id));
     protected $temp;                                                                                                     }
     protected $pic_queue = array();
     protected $pic_cached = array();
                                                                                                                         $this->public_timeline_timeout = Gtk::timeout_add(61000,,
     protected $public_timeline_timeout;                                                                                   array($this, 'update_public_timeline')); // every 60 seconds
     protected $load_images_timeout;
     public function __construct() {
         parent::__construct();
         $this->set_icon($this->render_icon(Gtk::STOCK_ABOUT, Gtk::ICON_SIZE_DIALOG));                                   $vbox = new GtkVBox();
         $this->set_size_request(300, 500);                                                                              $this->add($vbox);
         $this->set_title('PHP-GTK Twitter Client');
         $this->connect_simple('destroy', array('Gtk', 'main_quit'));                                                    $vbox->pack_start($tb, false, false);
                                                                                                                         $scrolled = new GtkScrolledWindow();
           $this->statusicon = new Php_Gtk_Twitter_Icon;                                                                 $scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
           $this->statusicon->connect('activate', array($this->statusicon,
                 'activate_window'), $this);                                                                             $vbox->pack_start($scrolled);
           $this->set_skip_taskbar_hint(true);                                                                           $this->treeview = new GtkTreeView($store);
           $this->connect('window-state-event', array($this, 'minimize_to_tray'));
                                                                                                                         $scrolled->add($this->treeview);
           $this->temp = sys_get_temp_dir() . 'php-gtk-twitter-api-cache';                                             $this->treeview->set_property('headers-visible', false);
           if (!file_exists($this->temp)) {
                 mkdir($this->temp, null, true);                                                                         $this->treeview->set_rules_hint(true);
           }                                                                                                             $vbox->pack_start(new GtkLabel('What are you doing?'), false, false);
           $this->twitter = new Php_Gtk_Twitter_Api;                                                                     $vbox->pack_start($this->updateentry, false, false);
           $this->statusbar = new GtkStatusBar();                                                                        $vbox->pack_start($this->entrystatus, false, false);
           // Create a toolbar with login button                                                                         $vbox->pack_start($this->statusbar, false, false);
           $tb = new GtkToolbar();
           $tb->set_show_arrow(false);                                                                                   $picture_renderer = new GtkCellRendererPixbuf();
           $this->loginbutton = GtkToolButton::new_from_stock(Gtk::STOCK_JUMP_TO);
           $this->loginbutton->set_label('Login');                                                                       $picture_column = new GtkTreeViewColumn('Picture',
           $this->loginbutton->connect_simple('clicked', array($this, 'login'));                                              $picture_renderer, 'pixbuf', 0);
           $tb->insert($this->loginbutton, -1);
                                                                                                                         $picture_column->set_cell_data_func($picture_renderer,
           // logout button, hide it                                                                                        array($this, 'show_user'));
           $this->logoutbutton = GtkToolButton::new_from_stock(Gtk::STOCK_CLOSE);
           $this->logoutbutton->set_label('Logout');                                                                     $this->treeview->append_column($picture_column);
           $this->logoutbutton->connect_simple('clicked', array($this, 'logout'));
           $tb->insert($this->logoutbutton, -1);
           $this->logoutbutton->set_sensitive(false);                                                                    $message_renderer = new GtkCellRendererText();
                                                                                                                         $message_renderer->set_property('wrap-mode', Gtk::WRAP_WORD);
           // Create an update area                                                                                      $message_renderer->set_property('wrap-width', 200);
           $this->updateentry = new GtkEntry();
           $this->updateentry->set_max_length(140);                                                                      $message_renderer->set_property('width', 10);
           $this->updateentry->set_sensitive(false);
           $this->updateentry->connect('activate', array($this, 'send_update'));
           $this->entrystatus = new GtkLabel();                                                                          $message_column = new GtkTreeViewColumn('Message',
                                                                                                                           $message_renderer);
           // User image pixbuf, user image string, user name, user id, text, favorited, created_at, id
           $store = new GtkListStore(GdkPixbuf::gtype, Gobject::TYPE_STRING, Gobject::TYPE_STRING,                       $message_column->set_cell_data_func($message_renderer,
                 Gobject::TYPE_LONG, GObject::TYPE_STRING, GObject::TYPE_BOOLEAN                                            array($this, 'message_markup'));
              , GObject::TYPE_STRING,                                                                                    $this->treeview->append_column($message_column);
                 Gobject::TYPE_LONG);
           $store->set_sort_column_id(7, Gtk::SORT_DESCENDING);
                                                                                                                         $this->treeview->set_resize_mode(Gtk::RESIZE_IMMEDIATE);
           $list = $this->twitter->get_public_timeline();
           $this->statusbar->push(1, 'last updated ' . date('Y-m-d H:i') . ' - ' . count($list) . ' new tweets');   }
COMPLETED CODE
Final Code, all 492 lines
public function show_user($column, $cell, $store, $position) {                protected function distance($from) {
            $pic = $store->get_value($position, 1);                                     $minutes = round(abs(time() - strtotime($from)) / 60);
            $name = $this->temp . md5($pic);
            if (isset($this->pic_queue[$name])) {                                       switch(true) {
                   return;                                                                     case ($minutes == 0):
            } elseif (isset($this->pic_cached[$name])) {                                              return 'less than 1 minute ago';
                   $store = $this->treeview->get_model();                                      case ($minutes < 1):
                   if (is_null($store->get_value($position, 0))) {
                          $pixbuf = GdkPixbuf::new_from_file($name . '.jpg');                         return '1 minute ago';
                          $store->set($position, 0, $pixbuf);                                  case ($minutes <= 55):
                          $cell->set_property('pixbuf', $pixbuf);                                     return $minutes . ' minutes ago';
                   }                                                                           case ($minutes <= 65):
                   return;                                                                            return 'about 1 hour ago';
            }                                                                                  case ($minutes <= 1439):
            $this->pic_queue[$name] = array('name' => $name, 'url' => $pic,                           return 'about ' . round((float) $minutes / 60.0) . ' hours';
                   'pos' => $position, 'cell' => $cell);                                       case ($minutes <= 2879):
            if (empty($this->load_images_timeout)) {
                   $this->load_images_timeout = Gtk::timeout_add(500,                                 return '1 day ago';
                                                                                               default:
                    array($this, 'pic_queue'));
            }                                                                                         return 'about ' . round((float) $minutes / 1440) . ' days ago';
      }                                                                                 }
                                                                                  }
      public function pic_queue() {
            $pic = array_shift($this->pic_queue);                                 public function minimize_to_tray($window, $event) {
            if (empty($pic)) {                                                          if ($event->changed_mask == Gdk::WINDOW_STATE_ICONIFIED &&
                   $this->load_images_timeout = null;                                          $event->new_window_state & Gdk::WINDOW_STATE_ICONIFIED) {
                   return true;                                                                $window->hide();
            }
            if (!file_exists($pic['name'])) {                                           }
                   file_put_contents($pic['name'] . '.jpg',                             return true; //stop bubbling
                    file_get_contents($pic['url']));                              }
                   $this->pic_cached[$pic['name']] = $pic['url'];
            }                                                                     public function update_public_timeline() {
            return true; // keep the timeout going                                      $this->pic_queue = array();
      }                                                                                 $list = $this->twitter->get_public_timeline();
                                                                                        $this->statusbar->pop(1);
      public function message_markup($column, $cell, $store,                            $this->statusbar->push(1, 'last updated ' .
          $position) {                                                                     date('Y-m-d H:i') . ' - ' . count($list) . ' new tweets');
            $user = utf8_decode($store->get_value($position, 2));                       $store = $this->treeview->get_model();
            $message = utf8_decode($store->get_value($position, 4));                    $store->clear();
            $time = $this->distance($store->get_value($position, 6));
                                                                                        foreach($list as $object) {
            $message = htmlspecialchars_decode($message, ENT_QUOTES);                          $store->append(array(null, $object->user->profile_image_url,
            $message = str_replace(array('@' . $user, '&nbsp;', '&'), array(                        $object->user->name, $object->user->id, $object->text,
              '<span foreground="#FF6633">@'                                                       $object->favorited, $object->created_at,
                . $user . '</span>', ' ', '&amp;'), $message);                                        $object->id));
            $cell->set_property('markup',                                               }
                "<b>$user</b>:n$messagen<small>$time</small>");                       return true;
      }                                                                           }
COMPLETED CODE
 Final Code, all 492 lines
public function update_timeline() {                                                 public function logout() {
            $list = $this->twitter->get_timeline();
            $this->statusbar->pop(1);                                                      $this->twitter->logout();
            $this->statusbar->push(1, 'last updated ' . date('Y-m-d H:i') . ' - '          $this->logoutbutton->set_sensitive(false);
                . count($list) . ' new tweets');                                           $this->loginbutton->set_sensitive(true);
            $store = $this->treeview->get_model();
            foreach($list as $object) {                                                    $this->public_timeline_timeout = Gtk::timeout_add(61000, array($this,
                    $store->append(array(null, $object->user->profile_image_url,              'update_public_timeline')); // every 60 seconds
                         $object->user->name, $object->user->id, $object->text,            $this->pic_queue = array();
                         $object->favorited, $object->created_at,                          $this->pic_cached = array();
                          $object->id));
            }                                                                              $this->update_public_timeline();
            return true;                                                                   $this->updateentry->set_sensitive(false);
      }                                                                                }
     public function login() {
         if (!empty($this->load_images_timeout)) {                                     public function send_update($entry) {
               Gtk::timeout_remove($this->load_images_timeout);
               $readd = true;                                                                    if ($this->twitter->send($entry->get_text())) {
         }                                                                                              $this->entrystatus->set_text('Message Sent');
         Gtk::timeout_remove($this->public_timeline_timeout);                                           $this->update_timeline();
         $login = new Php_Gtk_Twitter_Login_Dialog($this);
         while($response = $login->run()) {                                                             $this->updateentry->set_text('');
               if ($response == GTK::RESPONSE_CANCEL ||                                          } else {
                     $response == GTK::RESPONSE_DELETE_EVENT) {                                         $this->entrystatus->set_markup('<span color="red">
                      if (isset($readd)) {
                            $this->load_images_timeout = Gtk::timeout_add(500,                           Error Sending Message - Try Again</span>');
                                 array($this, 'pic_queue'));                                     }
                      }                                                                    }
                      $this->public_timeline_timeout = Gtk::timeout_add(61000,
                         array($this, 'update_public_timeline')); // every 60 seconds
                      $login->destroy();                                                   public function __destruct() {
                      break;                                                                     foreach(scandir($this->temp) as $filename) {
               } elseif ($response == GTK::RESPONSE_OK) {                                               if ($filename[0] == '.')
                      if($login->check_login($this->twitter)) {
                            $this->logoutbutton->set_sensitive(true);                                   continue;
                            $this->loginbutton->set_sensitive(false);                                   if (file_exists($this->temp . $filename)) {
                            $login->destroy();
                            $this->public_timeline_timeout = Gtk::timeout_add(61000,                          unlink($this->temp . $filename);
                              array($this, 'update_timeline')); // every 60 seconds                     }
                            $this->load_images_timeout = Gtk::timeout_add(500,                   }
                            array($this, 'pic_queue'));                                    }
                            $this->treeview->get_model()->clear();
                            $this->pic_queue = array();                               }
                            $this->pic_cached = array();                              $window = new Php_Gtk_Twitter_Client;
                            $this->update_timeline();                                 $window->show_all();
                            $this->updateentry->set_sensitive(true);
                            break;                                                    Gtk::main();
                      }
               }
         }
     }
RESOURCES
Wrapping it up

• Slides
      • http://callicore.net/php-gtk/php-on-the-desktop.pdf
• Code
      • http://callicore.net/php-gtk/php-gtk-twitter.zip
• GTK docs
      • http://gtk.org
      • http://pygtk.org
      • http://gtk.php.net/docs.php
      • http://leonpegg.com/php-gtk-doc/phpweb/
      • http://kksou.com
      • http://oops.opsat.net
      • http://php-gtk.eu
• Me
      • http://elizabethmariesmith.com
      • http://callicore.net
      • http://callicore.net/php-gtk
      • auroraeosrose@php.net
                                       THANKS
Ad

Recommended

Php on the Web and Desktop
Php on the Web and Desktop
Elizabeth Smith
 
Spl to the Rescue - Zendcon 09
Spl to the Rescue - Zendcon 09
Elizabeth Smith
 
SPL to the Rescue - Tek 09
SPL to the Rescue - Tek 09
Elizabeth Smith
 
Php go vrooom!
Php go vrooom!
Elizabeth Smith
 
Writing and using php streams and sockets tek11
Writing and using php streams and sockets tek11
Elizabeth Smith
 
Spl in the wild
Spl in the wild
Elizabeth Smith
 
PECL Picks - Extensions to make your life better
PECL Picks - Extensions to make your life better
ZendCon
 
Streams, sockets and filters oh my!
Streams, sockets and filters oh my!
Elizabeth Smith
 
Php on Windows
Php on Windows
Elizabeth Smith
 
Understanding PHP objects
Understanding PHP objects
julien pauli
 
Ant
Ant
sundar22in
 
Php tips-and-tricks4128
Php tips-and-tricks4128
PrinceGuru MS
 
Anatomy of a reusable module
Anatomy of a reusable module
Alessandro Franceschi
 
Php security3895
Php security3895
PrinceGuru MS
 
Supercharging WordPress Development in 2018
Supercharging WordPress Development in 2018
Adam Tomat
 
Auto-loading of Drupal CCK Nodes
Auto-loading of Drupal CCK Nodes
nihiliad
 
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
Fernando Hamasaki de Amorim
 
Puppet @ Seat
Puppet @ Seat
Alessandro Franceschi
 
Apache Velocity 1.6
Apache Velocity 1.6
Henning Schmiedehausen
 
Zephir - A Wind of Change for writing PHP extensions
Zephir - A Wind of Change for writing PHP extensions
Mark Baker
 
Power of Puppet 4
Power of Puppet 4
Martin Alfke
 
Php in 2013 (Web-5 2013 conference)
Php in 2013 (Web-5 2013 conference)
julien pauli
 
MIND sweeping introduction to PHP
MIND sweeping introduction to PHP
BUDNET
 
Puppet for Sys Admins
Puppet for Sys Admins
Puppet
 
Aura Project for PHP
Aura Project for PHP
Hari K T
 
Doctrine 2.0 Enterprise Persistence Layer for PHP
Doctrine 2.0 Enterprise Persistence Layer for PHP
Guilherme Blanco
 
Puppet Camp Berlin 2014: Manageable puppet infrastructure
Puppet Camp Berlin 2014: Manageable puppet infrastructure
Puppet
 
Introduction to Perl
Introduction to Perl
Krasimir Berov (Красимир Беров)
 
Php Inside - confoo 2011 - Derick Rethans
Php Inside - confoo 2011 - Derick Rethans
Bachkoutou Toutou
 
Pecl Picks
Pecl Picks
Elizabeth Smith
 

More Related Content

What's hot (20)

Php on Windows
Php on Windows
Elizabeth Smith
 
Understanding PHP objects
Understanding PHP objects
julien pauli
 
Ant
Ant
sundar22in
 
Php tips-and-tricks4128
Php tips-and-tricks4128
PrinceGuru MS
 
Anatomy of a reusable module
Anatomy of a reusable module
Alessandro Franceschi
 
Php security3895
Php security3895
PrinceGuru MS
 
Supercharging WordPress Development in 2018
Supercharging WordPress Development in 2018
Adam Tomat
 
Auto-loading of Drupal CCK Nodes
Auto-loading of Drupal CCK Nodes
nihiliad
 
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
Fernando Hamasaki de Amorim
 
Puppet @ Seat
Puppet @ Seat
Alessandro Franceschi
 
Apache Velocity 1.6
Apache Velocity 1.6
Henning Schmiedehausen
 
Zephir - A Wind of Change for writing PHP extensions
Zephir - A Wind of Change for writing PHP extensions
Mark Baker
 
Power of Puppet 4
Power of Puppet 4
Martin Alfke
 
Php in 2013 (Web-5 2013 conference)
Php in 2013 (Web-5 2013 conference)
julien pauli
 
MIND sweeping introduction to PHP
MIND sweeping introduction to PHP
BUDNET
 
Puppet for Sys Admins
Puppet for Sys Admins
Puppet
 
Aura Project for PHP
Aura Project for PHP
Hari K T
 
Doctrine 2.0 Enterprise Persistence Layer for PHP
Doctrine 2.0 Enterprise Persistence Layer for PHP
Guilherme Blanco
 
Puppet Camp Berlin 2014: Manageable puppet infrastructure
Puppet Camp Berlin 2014: Manageable puppet infrastructure
Puppet
 
Introduction to Perl
Introduction to Perl
Krasimir Berov (Красимир Беров)
 
Understanding PHP objects
Understanding PHP objects
julien pauli
 
Php tips-and-tricks4128
Php tips-and-tricks4128
PrinceGuru MS
 
Supercharging WordPress Development in 2018
Supercharging WordPress Development in 2018
Adam Tomat
 
Auto-loading of Drupal CCK Nodes
Auto-loading of Drupal CCK Nodes
nihiliad
 
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
The worst Ruby codes I’ve seen in my life - RubyKaigi 2015
Fernando Hamasaki de Amorim
 
Zephir - A Wind of Change for writing PHP extensions
Zephir - A Wind of Change for writing PHP extensions
Mark Baker
 
Php in 2013 (Web-5 2013 conference)
Php in 2013 (Web-5 2013 conference)
julien pauli
 
MIND sweeping introduction to PHP
MIND sweeping introduction to PHP
BUDNET
 
Puppet for Sys Admins
Puppet for Sys Admins
Puppet
 
Aura Project for PHP
Aura Project for PHP
Hari K T
 
Doctrine 2.0 Enterprise Persistence Layer for PHP
Doctrine 2.0 Enterprise Persistence Layer for PHP
Guilherme Blanco
 
Puppet Camp Berlin 2014: Manageable puppet infrastructure
Puppet Camp Berlin 2014: Manageable puppet infrastructure
Puppet
 

Similar to Php on the desktop and php gtk2 (20)

Php Inside - confoo 2011 - Derick Rethans
Php Inside - confoo 2011 - Derick Rethans
Bachkoutou Toutou
 
Pecl Picks
Pecl Picks
Elizabeth Smith
 
Can't Miss Features of PHP 5.3 and 5.4
Can't Miss Features of PHP 5.3 and 5.4
Jeff Carouth
 
Fatc
Fatc
Wade Arnold
 
Modern php
Modern php
Charles Anderson
 
Lca05
Lca05
Sateesh Patil
 
Facebook的缓存系统
Facebook的缓存系统
yiditushe
 
Lean Php Presentation
Lean Php Presentation
Alan Pinstein
 
Last 2 Months in PHP - July & August 2016
Last 2 Months in PHP - July & August 2016
Eric Poe
 
Phpwebdev
Phpwebdev
Luv'k Verma
 
Introduction to PHP 5.3
Introduction to PHP 5.3
guestcc91d4
 
When your code is nearly old enough to vote
When your code is nearly old enough to vote
dreamwidth
 
Php
Php
zalatarunk
 
PHP 5.3 Part 1 - Introduction to PHP 5.3
PHP 5.3 Part 1 - Introduction to PHP 5.3
melechi
 
Tips
Tips
mclee
 
CakePHP 2.0 - It'll rock your world
CakePHP 2.0 - It'll rock your world
Graham Weldon
 
phpwebdev.ppt
phpwebdev.ppt
rawaccess
 
PHP 5.3 Overview
PHP 5.3 Overview
jsmith92
 
Synapse india reviews on php website development
Synapse india reviews on php website development
saritasingh19866
 
4069180 Caching Performance Lessons From Facebook
4069180 Caching Performance Lessons From Facebook
guoqing75
 
Php Inside - confoo 2011 - Derick Rethans
Php Inside - confoo 2011 - Derick Rethans
Bachkoutou Toutou
 
Can't Miss Features of PHP 5.3 and 5.4
Can't Miss Features of PHP 5.3 and 5.4
Jeff Carouth
 
Facebook的缓存系统
Facebook的缓存系统
yiditushe
 
Lean Php Presentation
Lean Php Presentation
Alan Pinstein
 
Last 2 Months in PHP - July & August 2016
Last 2 Months in PHP - July & August 2016
Eric Poe
 
Introduction to PHP 5.3
Introduction to PHP 5.3
guestcc91d4
 
When your code is nearly old enough to vote
When your code is nearly old enough to vote
dreamwidth
 
PHP 5.3 Part 1 - Introduction to PHP 5.3
PHP 5.3 Part 1 - Introduction to PHP 5.3
melechi
 
Tips
Tips
mclee
 
CakePHP 2.0 - It'll rock your world
CakePHP 2.0 - It'll rock your world
Graham Weldon
 
phpwebdev.ppt
phpwebdev.ppt
rawaccess
 
PHP 5.3 Overview
PHP 5.3 Overview
jsmith92
 
Synapse india reviews on php website development
Synapse india reviews on php website development
saritasingh19866
 
4069180 Caching Performance Lessons From Facebook
4069180 Caching Performance Lessons From Facebook
guoqing75
 
Ad

More from Elizabeth Smith (20)

Welcome to the internet
Welcome to the internet
Elizabeth Smith
 
Database theory and modeling
Database theory and modeling
Elizabeth Smith
 
Taming the resource tiger
Taming the resource tiger
Elizabeth Smith
 
Modern sql
Modern sql
Elizabeth Smith
 
Php extensions
Php extensions
Elizabeth Smith
 
Taming the resource tiger
Taming the resource tiger
Elizabeth Smith
 
Php internal architecture
Php internal architecture
Elizabeth Smith
 
Taming the tiger - pnwphp
Taming the tiger - pnwphp
Elizabeth Smith
 
Php extensions
Php extensions
Elizabeth Smith
 
Php extensions
Php extensions
Elizabeth Smith
 
Php’s guts
Php’s guts
Elizabeth Smith
 
Lexing and parsing
Lexing and parsing
Elizabeth Smith
 
Hacking with hhvm
Hacking with hhvm
Elizabeth Smith
 
Security is not a feature
Security is not a feature
Elizabeth Smith
 
Using unicode with php
Using unicode with php
Elizabeth Smith
 
Mentoring developers-php benelux-2014
Mentoring developers-php benelux-2014
Elizabeth Smith
 
Using unicode with php
Using unicode with php
Elizabeth Smith
 
Socket programming with php
Socket programming with php
Elizabeth Smith
 
Mentoring developers
Mentoring developers
Elizabeth Smith
 
Do the mentor thing
Do the mentor thing
Elizabeth Smith
 
Ad

Recently uploaded (20)

OpenACC and Open Hackathons Monthly Highlights June 2025
OpenACC and Open Hackathons Monthly Highlights June 2025
OpenACC
 
Mastering AI Workflows with FME - Peak of Data & AI 2025
Mastering AI Workflows with FME - Peak of Data & AI 2025
Safe Software
 
FIDO Seminar: Targeting Trust: The Future of Identity in the Workforce.pptx
FIDO Seminar: Targeting Trust: The Future of Identity in the Workforce.pptx
FIDO Alliance
 
OWASP Barcelona 2025 Threat Model Library
OWASP Barcelona 2025 Threat Model Library
PetraVukmirovic
 
Data Validation and System Interoperability
Data Validation and System Interoperability
Safe Software
 
No-Code Workflows for CAD & 3D Data: Scaling AI-Driven Infrastructure
No-Code Workflows for CAD & 3D Data: Scaling AI-Driven Infrastructure
Safe Software
 
Floods in Valencia: Two FME-Powered Stories of Data Resilience
Floods in Valencia: Two FME-Powered Stories of Data Resilience
Safe Software
 
Enabling BIM / GIS integrations with Other Systems with FME
Enabling BIM / GIS integrations with Other Systems with FME
Safe Software
 
FME for Distribution & Transmission Integrity Management Program (DIMP & TIMP)
FME for Distribution & Transmission Integrity Management Program (DIMP & TIMP)
Safe Software
 
Edge-banding-machines-edgeteq-s-200-en-.pdf
Edge-banding-machines-edgeteq-s-200-en-.pdf
AmirStern2
 
National Fuels Treatments Initiative: Building a Seamless Map of Hazardous Fu...
National Fuels Treatments Initiative: Building a Seamless Map of Hazardous Fu...
Safe Software
 
High Availability On-Premises FME Flow.pdf
High Availability On-Premises FME Flow.pdf
Safe Software
 
June Patch Tuesday
June Patch Tuesday
Ivanti
 
AudGram Review: Build Visually Appealing, AI-Enhanced Audiograms to Engage Yo...
AudGram Review: Build Visually Appealing, AI-Enhanced Audiograms to Engage Yo...
SOFTTECHHUB
 
Providing an OGC API Processes REST Interface for FME Flow
Providing an OGC API Processes REST Interface for FME Flow
Safe Software
 
Raman Bhaumik - Passionate Tech Enthusiast
Raman Bhaumik - Passionate Tech Enthusiast
Raman Bhaumik
 
Tech-ASan: Two-stage check for Address Sanitizer - Yixuan Cao.pdf
Tech-ASan: Two-stage check for Address Sanitizer - Yixuan Cao.pdf
caoyixuan2019
 
Reducing Conflicts and Increasing Safety Along the Cycling Networks of East-F...
Reducing Conflicts and Increasing Safety Along the Cycling Networks of East-F...
Safe Software
 
FIDO Seminar: Perspectives on Passkeys & Consumer Adoption.pptx
FIDO Seminar: Perspectives on Passkeys & Consumer Adoption.pptx
FIDO Alliance
 
FME for Good: Integrating Multiple Data Sources with APIs to Support Local Ch...
FME for Good: Integrating Multiple Data Sources with APIs to Support Local Ch...
Safe Software
 
OpenACC and Open Hackathons Monthly Highlights June 2025
OpenACC and Open Hackathons Monthly Highlights June 2025
OpenACC
 
Mastering AI Workflows with FME - Peak of Data & AI 2025
Mastering AI Workflows with FME - Peak of Data & AI 2025
Safe Software
 
FIDO Seminar: Targeting Trust: The Future of Identity in the Workforce.pptx
FIDO Seminar: Targeting Trust: The Future of Identity in the Workforce.pptx
FIDO Alliance
 
OWASP Barcelona 2025 Threat Model Library
OWASP Barcelona 2025 Threat Model Library
PetraVukmirovic
 
Data Validation and System Interoperability
Data Validation and System Interoperability
Safe Software
 
No-Code Workflows for CAD & 3D Data: Scaling AI-Driven Infrastructure
No-Code Workflows for CAD & 3D Data: Scaling AI-Driven Infrastructure
Safe Software
 
Floods in Valencia: Two FME-Powered Stories of Data Resilience
Floods in Valencia: Two FME-Powered Stories of Data Resilience
Safe Software
 
Enabling BIM / GIS integrations with Other Systems with FME
Enabling BIM / GIS integrations with Other Systems with FME
Safe Software
 
FME for Distribution & Transmission Integrity Management Program (DIMP & TIMP)
FME for Distribution & Transmission Integrity Management Program (DIMP & TIMP)
Safe Software
 
Edge-banding-machines-edgeteq-s-200-en-.pdf
Edge-banding-machines-edgeteq-s-200-en-.pdf
AmirStern2
 
National Fuels Treatments Initiative: Building a Seamless Map of Hazardous Fu...
National Fuels Treatments Initiative: Building a Seamless Map of Hazardous Fu...
Safe Software
 
High Availability On-Premises FME Flow.pdf
High Availability On-Premises FME Flow.pdf
Safe Software
 
June Patch Tuesday
June Patch Tuesday
Ivanti
 
AudGram Review: Build Visually Appealing, AI-Enhanced Audiograms to Engage Yo...
AudGram Review: Build Visually Appealing, AI-Enhanced Audiograms to Engage Yo...
SOFTTECHHUB
 
Providing an OGC API Processes REST Interface for FME Flow
Providing an OGC API Processes REST Interface for FME Flow
Safe Software
 
Raman Bhaumik - Passionate Tech Enthusiast
Raman Bhaumik - Passionate Tech Enthusiast
Raman Bhaumik
 
Tech-ASan: Two-stage check for Address Sanitizer - Yixuan Cao.pdf
Tech-ASan: Two-stage check for Address Sanitizer - Yixuan Cao.pdf
caoyixuan2019
 
Reducing Conflicts and Increasing Safety Along the Cycling Networks of East-F...
Reducing Conflicts and Increasing Safety Along the Cycling Networks of East-F...
Safe Software
 
FIDO Seminar: Perspectives on Passkeys & Consumer Adoption.pptx
FIDO Seminar: Perspectives on Passkeys & Consumer Adoption.pptx
FIDO Alliance
 
FME for Good: Integrating Multiple Data Sources with APIs to Support Local Ch...
FME for Good: Integrating Multiple Data Sources with APIs to Support Local Ch...
Safe Software
 

Php on the desktop and php gtk2

  • 1. PHP ON THE DESKTOP AND PHP-GTK2 Elizabeth Marie Smith – Zend Uncon 2008
  • 2. ABOUT ME Elizabeth Marie Smith • AKA auroraeosrose • Have a very very common name • Was an English Major (minor in Sociology) • Started using PHP for a Manga/Anime fan site (still alive) • Have been using PHP since 4 beta 1 (23-Jul-1999) • Have been using C since Nov of 2006 • Love the color Pink • I hate computers • I love programming • Thinks open source on Windows is a “gateway drug” • Works at OmniTI (http://omniti.com) • Am a contributor on various open source projects (including PHP, PECL and PHP-GTK)
  • 4. THE DESKTOP STILL LIVES Reasons For Desktop Applications • People aren’t always online • Some applications would be bandwidth prohibitive • Some data is too sensitive to be passed on the wire • Embedded OSs don’t always work well with websites • Browsers are just Quirky (I’m looking at you, IE6, IE7, Opera…) • Current Desktop RIA tools (AIR, Xulrunner, Mozilla Prism) are still young (and buggy)
  • 5. WHY USE PHP FOR DESKTOP APPLICATIONS PHP Advantages • No Compiling • Instant Changes • No learning curve for a new language • Plug into PHP extensions • Easy to Use • Fast to Write • Use already familiar libraries and tools
  • 6. WHY USE PHP FOR DESKTOP APPLICATIONS PHP Disadvantages • Speed • Additional Extensions/Libraries needed • Speed • Distribution (phar is helpful there) • Speed • Security (of code – source code encoders might help here) • No threading See the theme?
  • 7. HOW DO I WRITE DESKTOP APPS? PHP CLI • PHP does not care about HTML (really) • PHP works through SAPIS (Server Application Programming Interface) – for the desktop the CLI (Command Line Interface) SAPI is our friend • CLI has no headers, CLI responds to no requests • You need a way to make GUI’s if you don’t want a console app – Just like we wrap other C/C++ libraries we can wrap GUI libraries
  • 8. GUI OPTIONS WITH PHP So What are your Options? • Well Established • PHP-GTK2 • Winbinder • Works, but not Complete • PHP-QT (no windows support) • WxWidgets (only windows support) • Win::Gui (part of Win::API) Bottom line? For Cross-Platform PHP-GTK works
  • 9. WTH IS GTK? Quick Intro • GTK was Gimp Tool Kit (long long ago) • GTK is GUI Toolkit for Gnome (if you use gnome, you have it already) • GTK has multiple backends (for native Windows and MacOSX apps – you don’t need X on Mac anymore) • To use PHP-GTK you need PHP CLI, GTK, and the PHP-GTK extension • http://oops.opsat.net/doc/install.html - how to install (windows is unzip and run, mac has an installer, ubuntu has .debs – yes this should be in the php-gtk docs, we need volunteers who aren’t afraid of xml)
  • 11. THE BASICS What you need to know to use PHP-GTK • Main Loop • Signals & Callbacks • Widgets • OOP to the Max • Visibility
  • 12. STARTING OUR PROJECT Create Our initial Window Resulting window <?php class Php_Gtk_Twitter_Client extends GtkWindow { public function __construct() { parent::__construct(); $this->set_icon($this->render_icon( Gtk::STOCK_ABOUT, Gtk::ICON_SIZE_DIALOG)); $this->set_size_request(300, 500); $this->set_title('PHP-GTK Twitter Client'); $this->connect_simple('destroy', array('Gtk', 'main_quit')); } } $window = new Php_Gtk_Twitter_Client; $window->show_all(); Gtk::main();
  • 13. OVERRIDING BASIC FUNCTIONALITY Minimize to the Taskbar • Specialty Widgets are Cool • GtkStatusIcon – 2.10+ • Know your Hierarchy • Stock Icons • Callback “bubbling” • timeouts (timers)
  • 15. Minimize to Status Icon MINIMIZE TO TRAY <?php Resulting Window class Php_Gtk_Twitter_Icon extends GtkStatusIcon { protected $alive; protected $lockout; public function __construct() { parent::__construct(); $this->set_from_stock(Gtk::STOCK_ABOUT); $this->set_tooltip('PHP-GTK Twitter Client'); while(Gtk::events_pending() || Gdk::events_pending()) { Gtk::main_iteration_do(true); } $this->is_ready(); return; } public function is_ready() { $this->alive = true; if($this->lockout < 5 && !$this->is_embedded()) { Gtk::timeout_add(750,array($this,'is_ready')); ++$this->lockout; $this->alive = false; } else if(!$this->is_embedded()) { die("Error: Unable to create Tray Icon. Please insure that your system's tray is enabled.n"); $this->alive = false; } return; } public function activate_window($icon, $window) { if ($window->is_visible()) { $window->hide(); } else { $window->deiconify(); $window->show(); } } } class Php_Gtk_Twitter_Client extends GtkWindow { protected $statusicon; public function __construct() { parent::__construct(); $this->set_icon($this->render_icon(Gtk::STOCK_ABOUT, Gtk::ICON_SIZE_DIALOG)); $this->set_size_request(300, 500); $this->set_title('PHP-GTK Twitter Client'); $this->connect_simple('destroy', array('Gtk', 'main_quit')); $this->statusicon = new Php_Gtk_Twitter_Icon; $this->statusicon->connect('activate', array($this->statusicon, 'activate_window'), $this); $this->set_skip_taskbar_hint(true); $this->connect('window-state-event', array($this, 'minimize_to_tray')); } public function minimize_to_tray($window, $event) { if ($event->changed_mask == Gdk::WINDOW_STATE_ICONIFIED && $event->new_window_state & Gdk::WINDOW_STATE_ICONIFIED) { $window->hide(); } return true; //stop bubbling } } $window = new Php_Gtk_Twitter_Client; $window->show_all(); Gtk::main();
  • 16. VIEWING DATA Data from PHP + a Treeview from GTK • Remember this is PHP • Treeview has a model and view • ListStore is a single level model • Treeviews need Columns • Columns need Renderers • Different types of Renderers
  • 17. MY TWITTER API HTTP Streams are GOOD I really don’t believe in wheel inventing , especially when I have little experience with the subject. However there is an issue – every existing twitter API uses curl – and I don’t want to depend on an extension that isn’t always installed. protected function process($url, $date = 0, $type = 'GET', $data = null) { // add caching header $this->headers[0] = 'If-Modified-Since: ' . date(DATE_RFC822, $date); $options = array( 'http' => array( 'method' => $type, 'header' => $this->headers) ); if (!is_null($data)) { $options['http']['content'] = http_build_query($data); } $context = stream_context_create($options); if ($this->username && $this->password) { $base = 'http://' . urlencode($this->username) . ':' . urlencode($this->password) . '@twitter.com/'; } else { $base = 'http://twitter.com/'; } set_error_handler(array($this,'swallow_error')); $string = file_get_contents($base . $url, false, $context); restore_error_handler(); return json_decode($string); }
  • 18. MINIMIZE TO TRAY Put a Treeview in the Window – this goes in __construct Callbacks for showing Images and Messages $this->temp = sys_get_temp_dir() . 'php-gtk-twitter-api-cache'; public function show_user($column, $cell, $store, $position) { if (!file_exists($this->temp)) { $pic = $store->get_value($position, 1); $name = $this->temp . md5($pic); mkdir($this->temp, null, true); if (isset($this->pic_queue[$name])) { } return; } elseif (isset($this->pic_cached[$name])) { $store = $this->treeview->get_model(); $this->twitter = new Php_Gtk_Twitter_Api; if (is_null($store->get_value($position, 0))) { $pixbuf = GdkPixbuf::new_from_file($name . '.jpg'); // User image pixbuf, user image string, user name, $store->set($position, 0, $pixbuf); $cell->set_property('pixbuf', $pixbuf); // user id, text, favorited, created_at, id } $store = new GtkListStore(GdkPixbuf::gtype, Gobject::TYPE_STRING, return; } Gobject::TYPE_STRING, Gobject::TYPE_LONG, GObject::TYPE_STRING, $this->pic_queue[$name] = array('name' => $name, 'url' => $pic, GObject::TYPE_BOOLEAN, GObject::TYPE_STRING, Gobject::TYPE_LONG); 'pos' => $position, 'cell' => $cell); if (empty($this->load_images_timeout)) { $store->set_sort_column_id(7, Gtk::SORT_DESCENDING); $this->load_images_timeout = Gtk::timeout_add(500, array($this, 'pic_queue')); } $list = $this->twitter->get_public_timeline(); } public function pic_queue() { // stuff the store $pic = array_shift($this->pic_queue); foreach($list as $object) { if (empty($pic)) { $store->append(array(null, $object->user->profile_image_url, $object->user->name, $this->load_images_timeout = null; return true; $object->user->id, $object->text, $object->favorited, $object->created_at, } $object->id)); if (!file_exists($pic['name'])) { } file_put_contents($pic['name'] . '.jpg', file_get_contents($pic['url'])); $this->pic_cached[$pic['name']] = $pic['url']; } $this->public_timeline_timeout = Gtk::timeout_add(61000, return true; // keep the timeout going array($this, 'update_public_timeline')); // every 60 seconds } public function message_markup($column, $cell, $store, $position) { $scrolled = new GtkScrolledWindow(); $user = utf8_decode($store->get_value($position, 2)); $message = utf8_decode($store->get_value($position, 4)); $scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); $time = $this->distance($store->get_value($position, 6)); $this->add($scrolled); $this->treeview = new GtkTreeView($store); $message = htmlspecialchars_decode($message, ENT_QUOTES); $scrolled->add($this->treeview); $message = str_replace(array('@' . $user, '&nbsp;', '&'), array('<span foreground="#FF6633">@' . $this->treeview->set_property('headers-visible', false); $user . '</span>', ' ', '&amp;'), $message); $cell->set_property('markup', "<b>$user</b>:n$messagen<small>$time</small>"); $this->treeview->set_rules_hint(true); } $picture_renderer = new GtkCellRendererPixbuf(); protected function distance($from) { $minutes = round(abs(time() - strtotime($from)) / 60); $picture_column = new GtkTreeViewColumn('Picture', $picture_renderer, 'pixbuf', 0); $picture_column->set_cell_data_func($picture_renderer, array($this, 'show_user')); switch(true) { $this->treeview->append_column($picture_column); case ($minutes == 0): return 'less than 1 minute ago'; case ($minutes < 1): $message_renderer = new GtkCellRendererText(); return '1 minute ago'; $message_renderer->set_property('wrap-mode', Gtk::WRAP_WORD); case ($minutes <= 55): return $minutes . ' minutes ago'; $message_renderer->set_property('wrap-width', 200); case ($minutes <= 65): $message_renderer->set_property('width', 10); return 'about 1 hour ago'; case ($minutes <= 1439): $message_column = new GtkTreeViewColumn('Message', $message_renderer); return 'about ' . round((float) $minutes / 60.0) . ' hours'; case ($minutes <= 2879): $message_column->set_cell_data_func($message_renderer, return '1 day ago'; array($this, 'message_markup')); default: return 'about ' . round((float) $minutes / 1440) . ' days ago'; $this->treeview->append_column($message_column); } } $this->treeview->set_resize_mode(Gtk::RESIZE_IMMEDIATE);
  • 19. A WORKING VIEW OF TWITTER MESSAGES It works! Public Timeline in Action
  • 20. PACKING Putting other things in a Window • GTK is not “pixel perfect” • Packing is not as hard as it looks • Containers can hold one or many • Remember that a container expands to the size of it’s contents
  • 21. TOOLBARS AND STATUSBARS Pack the Statusbar, Treeview, and Toolbar in a VBox Create Statusbar and Toolbar $this->statusbar = new GtkStatusBar(); // Create a toolbar with login button $tb = new GtkToolbar(); $tb->set_show_arrow(false); $this->loginbutton = GtkToolButton::new_from_stock(Gtk::STOCK_JUMP_TO); $this->loginbutton->set_label('Login'); $this->loginbutton->connect_simple('clicked', array($this, 'login')); $tb->insert($this->loginbutton, -1); // logout button $this->logoutbutton = GtkToolButton::new_from_stock(Gtk::STOCK_CLOSE); $this->logoutbutton->set_label('Logout'); $this->logoutbutton->connect_simple('clicked', array($this, 'logout')); $tb->insert($this->logoutbutton, -1); $this->logoutbutton->set_sensitive(false); With Toolbar, Treeview, and Statusbar $vbox = new GtkVBox(); $this->add($vbox); $vbox->pack_start($tb, false, false); $vbox->pack_start($scrolled); $vbox->pack_start($this->statusbar, false, false); Update the Header and Public Timeline using a Gtk::timeout public function update_public_timeline() { $this->pic_queue = array(); $list = $this->twitter->get_public_timeline(); $this->statusbar->pop(1); $this->statusbar->push(1, 'last updated ' . date('Y-m-d H:i') . ' ' . count($list) . ' new tweets'); $store = $this->treeview->get_model(); $store->clear(); foreach($list as $object) { $store->append(array(null, $object->user->profile_image_url, $object->user->name, $object->user->id, $object->text, $object->favorited, $object->created_at, $object->id)); } return true; }
  • 22. DIALOGS Additional Windows • Dialogs are cool • Dialogs are usually modal, but not always • Dialogs can be very general or very specific • You can put anything inside one
  • 23. LOGIN DIALOG BOX Extends GtkDialog Login Dialog class Php_Gtk_Twitter_Login_Dialog extends GtkDialog { protected $emailentry; protected $passwordentry; public function __construct($parent) { parent::__construct('Login to Twitter', $parent, Gtk::DIALOG_MODAL, array( Gtk::STOCK_OK, Gtk::RESPONSE_OK, Gtk::STOCK_CANCEL, Gtk::RESPONSE_CANCEL)); $table = new GtkTable(); $email = new GtkLabel('Email:'); $table->attach($email, 0, 1, 0, 1); $password = new GtkLabel('Password:'); $table->attach($password, 0, 1, 1, 2); $this->emailentry = new GtkEntry(); $table->attach($this->emailentry, 1, 2, 0, 1); $this->passwordentry = new GtkEntry(); $table->attach($this->passwordentry, 1, 2, 1, 2); $this->passwordentry->set_visibility(false); $this->vbox->add($table); $this->errorlabel = new GtkLabel(); $this->vbox->add($this->errorlabel); $this->show_all(); } public function check_login($twitter) { $this->errorlabel->set_text(''); $email = $this->emailentry->get_text(); $password = $this->passwordentry->get_text(); if (empty($password) || empty($password)) { $this->errorlabel->set_markup( '<span color="red">Name and Password must be entered</span>'); return false; } if ($twitter->login($email, $password)) { return true; } else { $this->errorlabel->set_markup( '<span color="red">Authentication Error</span>'); return false; } } }
  • 24. LOGIN DIALOG BOX Callbacks After Login public function login() { if (!empty($this->load_images_timeout)) { Gtk::timeout_remove($this->load_images_timeout); $readd = true; } Gtk::timeout_remove($this->public_timeline_timeout); $login = new Php_Gtk_Twitter_Login_Dialog($this); while($response = $login->run()) { if ($response == GTK::RESPONSE_CANCEL || $response == GTK::RESPONSE_DELETE_EVENT) { if (isset($readd)) { $this->load_images_timeout = Gtk::timeout_add(500, array($this, 'pic_queue')); } $this->public_timeline_timeout = Gtk::timeout_add(61000, array($this, 'update_public_timeline')); $login->destroy(); break; } elseif ($response == GTK::RESPONSE_OK) { if($login->check_login($this->twitter)) { $this->logoutbutton->set_sensitive(true); $this->loginbutton->set_sensitive(false); $login->destroy(); $this->public_timeline_timeout = Gtk::timeout_add( 61000, array($this, 'update_timeline')); $this->load_images_timeout = Gtk::timeout_add(500, array($this, 'pic_queue')); $this->treeview->get_model()->clear(); $this->pic_queue = array(); $this->pic_cached = array(); $this->update_timeline(); break; } } } } public function logout() { $this->twitter->logout(); $this->logoutbutton->set_sensitive(false); $this->loginbutton->set_sensitive(true); $this->public_timeline_timeout = Gtk::timeout_add(61000, array($this, 'update_public_timeline')); // every 60 seconds $this->pic_queue = array(); $this->pic_cached = array(); $this->update_public_timeline(); }
  • 25. DATA ENTRY Putting in Data • GTKEntry • Basic Data Entry – activates on return, can set maximum length allowed • Simple label for messages – could use a dialog or other method of informing the user
  • 26. LOGIN DIALOG BOX Packing it in Creating the Entries $vbox = new GtkVBox(); // Create an update area $this->add($vbox); $this->updateentry = new GtkEntry(); $vbox->pack_start($tb, false, false); $this->updateentry->set_max_length(140); $scrolled = new GtkScrolledWindow(); $this->updateentry->set_sensitive(false); $scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); $this->updateentry->connect('activate', $vbox->pack_start($scrolled); array($this, 'send_update')); $vbox->pack_start(new GtkLabel('What are you doing?'), false, false); $this->entrystatus = new GtkLabel(); $vbox->pack_start($this->updateentry, false, false); $vbox->pack_start($this->entrystatus, false, false); $vbox->pack_start($this->statusbar, false, false); The Callback on Activate public function send_update($entry) { if ($this->twitter->send($entry->get_text())) { $this->entrystatus->set_text('Message Sent'); $this->update_timeline(); $this->updateentry->set_text(''); } else { $this->entrystatus->set_markup(‘ <span color="red">Error Sending Message - Try Again</span>'); } }
  • 28. COMPLETED CODE Final Code, all 492 lines class Php_Gtk_Twitter_Api { public function get_timeline() { protected $login = false; if ($this->login && $this->can_call()) { protected $username; if (empty($this->lastid)) { protected $password; $data = $this->process('statuses/friends_timeline.json'); } else { $data = $this->process('statuses/friends_timeline.json', $this->lasttime, protected $cached_public; 'GET', array('since_id' => $this->lastid)); } protected $cached_public_timestamp = 0; if ($data) { $this->lastid = $data[0]->id; } protected $lastid = 0; $this->lasttime = time(); return $data; protected $headers = array } } ('', 'X-Twitter-Client: PHP-GTK Twitter Client', public function send($message) { 'X-Twitter-Client-Version: 0.1.0-dev', if ($this->login && $this->can_call()) { 'X-Twitter-Client-URL: http://elizabethmariesmith.com'); $data = $this->process('statuses/update.json', 0, 'POST', array('status' => $message)); if ($data) { return true; public function login($username, $password) { } $this->username = $username; return false; } $this->password = $password; } $worked = $this->process('account/verify_credentials.json'); protected function can_call() { if ($worked && $worked->authorized == true) { if (!$this->login) { $this->login = true; return false; } return true; $worked = $this->process('account/rate_limit_status.json'); } return ($worked->remaining_hits > 1); return false; } } protected function process($url, $date = 0, $type = 'GET', $data = null) { // add caching header $this->headers[0] = 'If-Modified-Since: ' . date(DATE_RFC822, $date); public function logout() { $this->username = null; $options = array( 'http' => array( $this->password = null; 'method' => $type, $this->login = false; 'header' => $this->headers) $this->process('account/end_session', 0, 'POST'); ); if (!is_null($data)) { } $options['http']['content'] = http_build_query($data); } $context = stream_context_create($options); public function get_public_timeline() { if ($this->username && $this->password) { if ($this->cached_public_timestamp < time()) { $base = 'http://' . urlencode($this->username) . ':' . urlencode($this->password) . '@twitter.com/'; $this->cached_public = } else { json_decode(file_get_contents( $base = 'http://twitter.com/'; } 'http://twitter.com/statuses/public_timeline.json')); set_error_handler(array($this,'swallow_error')); $this->cached_public_timestamp = time() + 60; $string = file_get_contents($base . $url, false, $context); restore_error_handler(); // caches every 60 seconds return json_decode($string); } } return $this->cached_public; public function swallow_error($errno, $errstr) {} // this should be treated as private } }
  • 29. COMPLETED CODE Final Code, all 492 lines class Php_Gtk_Twitter_Login_Dialog extends GtkDialog { class Php_Gtk_Twitter_Icon extends GtkStatusIcon { protected $emailentry; protected $passwordentry; protected $alive; protected $lockout; public function __construct($parent) { parent::__construct('Login to Twitter', public function __construct() { $parent, Gtk::DIALOG_MODAL, parent::__construct(); array(Gtk::STOCK_OK, Gtk::RESPONSE_OK, Gtk::STOCK_CANCEL, Gtk::RESPONSE_CANCEL)); $this->set_from_stock(Gtk::STOCK_ABOUT); $table = new GtkTable(); $this->set_tooltip('PHP-GTK Twitter Client'); $email = new GtkLabel('Email:'); $table->attach($email, 0, 1, 0, 1); while(Gtk::events_pending() || Gdk::events_pending()) { $password = new GtkLabel('Password:'); Gtk::main_iteration_do(true); $table->attach($password, 0, 1, 1, 2); } $this->emailentry = new GtkEntry(); $this->is_ready(); $table->attach($this->emailentry, 1, 2, 0, 1); return; $this->passwordentry = new GtkEntry(); } $table->attach($this->passwordentry, 1, 2, 1, 2); $this->passwordentry->set_visibility(false); public function is_ready() { $this->vbox->add($table); $this->alive = true; $this->errorlabel = new GtkLabel(); $this->vbox->add($this->errorlabel); if($this->lockout < 5 && !$this->is_embedded()) { $this->show_all(); Gtk::timeout_add(750,array($this,'is_ready')); } ++$this->lockout; $this->alive = false; public function check_login($twitter) { } else if(!$this->is_embedded()) { $this->errorlabel->set_text(''); die("Error: Unable to create Tray Icon. $email = $this->emailentry->get_text(); Please insure that your system's tray is enabled.n"); $password = $this->passwordentry->get_text(); $this->alive = false; if (empty($password) || empty($password)) { } $this->errorlabel->set_markup( '<span color="red">Name and Password must be entered</span>'); return; return false; } } if ($twitter->login($email, $password)) { return true; public function activate_window($icon, $window) { } else { if ($window->is_visible()) { $this->errorlabel->set_markup( $window->hide(); '<span color="red">Authentication Error</span>'); } else { return false; $window->deiconify(); } $window->show(); } } } }
  • 30. COMPLETED CODE Final Code, all 492 lines class Php_Gtk_Twitter_Client extends GtkWindow { foreach($list as $object) { $store->append(array(null, $object->user->profile_image_url, protected $statusicon; protected $twitter; $object->user->name, $object->user->id, protected $treeview; $object->text, $object->favorited, $object->created_at, protected $statusbar; $object->id)); protected $temp; } protected $pic_queue = array(); protected $pic_cached = array(); $this->public_timeline_timeout = Gtk::timeout_add(61000,, protected $public_timeline_timeout; array($this, 'update_public_timeline')); // every 60 seconds protected $load_images_timeout; public function __construct() { parent::__construct(); $this->set_icon($this->render_icon(Gtk::STOCK_ABOUT, Gtk::ICON_SIZE_DIALOG)); $vbox = new GtkVBox(); $this->set_size_request(300, 500); $this->add($vbox); $this->set_title('PHP-GTK Twitter Client'); $this->connect_simple('destroy', array('Gtk', 'main_quit')); $vbox->pack_start($tb, false, false); $scrolled = new GtkScrolledWindow(); $this->statusicon = new Php_Gtk_Twitter_Icon; $scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); $this->statusicon->connect('activate', array($this->statusicon, 'activate_window'), $this); $vbox->pack_start($scrolled); $this->set_skip_taskbar_hint(true); $this->treeview = new GtkTreeView($store); $this->connect('window-state-event', array($this, 'minimize_to_tray')); $scrolled->add($this->treeview); $this->temp = sys_get_temp_dir() . 'php-gtk-twitter-api-cache'; $this->treeview->set_property('headers-visible', false); if (!file_exists($this->temp)) { mkdir($this->temp, null, true); $this->treeview->set_rules_hint(true); } $vbox->pack_start(new GtkLabel('What are you doing?'), false, false); $this->twitter = new Php_Gtk_Twitter_Api; $vbox->pack_start($this->updateentry, false, false); $this->statusbar = new GtkStatusBar(); $vbox->pack_start($this->entrystatus, false, false); // Create a toolbar with login button $vbox->pack_start($this->statusbar, false, false); $tb = new GtkToolbar(); $tb->set_show_arrow(false); $picture_renderer = new GtkCellRendererPixbuf(); $this->loginbutton = GtkToolButton::new_from_stock(Gtk::STOCK_JUMP_TO); $this->loginbutton->set_label('Login'); $picture_column = new GtkTreeViewColumn('Picture', $this->loginbutton->connect_simple('clicked', array($this, 'login')); $picture_renderer, 'pixbuf', 0); $tb->insert($this->loginbutton, -1); $picture_column->set_cell_data_func($picture_renderer, // logout button, hide it array($this, 'show_user')); $this->logoutbutton = GtkToolButton::new_from_stock(Gtk::STOCK_CLOSE); $this->logoutbutton->set_label('Logout'); $this->treeview->append_column($picture_column); $this->logoutbutton->connect_simple('clicked', array($this, 'logout')); $tb->insert($this->logoutbutton, -1); $this->logoutbutton->set_sensitive(false); $message_renderer = new GtkCellRendererText(); $message_renderer->set_property('wrap-mode', Gtk::WRAP_WORD); // Create an update area $message_renderer->set_property('wrap-width', 200); $this->updateentry = new GtkEntry(); $this->updateentry->set_max_length(140); $message_renderer->set_property('width', 10); $this->updateentry->set_sensitive(false); $this->updateentry->connect('activate', array($this, 'send_update')); $this->entrystatus = new GtkLabel(); $message_column = new GtkTreeViewColumn('Message', $message_renderer); // User image pixbuf, user image string, user name, user id, text, favorited, created_at, id $store = new GtkListStore(GdkPixbuf::gtype, Gobject::TYPE_STRING, Gobject::TYPE_STRING, $message_column->set_cell_data_func($message_renderer, Gobject::TYPE_LONG, GObject::TYPE_STRING, GObject::TYPE_BOOLEAN array($this, 'message_markup')); , GObject::TYPE_STRING, $this->treeview->append_column($message_column); Gobject::TYPE_LONG); $store->set_sort_column_id(7, Gtk::SORT_DESCENDING); $this->treeview->set_resize_mode(Gtk::RESIZE_IMMEDIATE); $list = $this->twitter->get_public_timeline(); $this->statusbar->push(1, 'last updated ' . date('Y-m-d H:i') . ' - ' . count($list) . ' new tweets'); }
  • 31. COMPLETED CODE Final Code, all 492 lines public function show_user($column, $cell, $store, $position) { protected function distance($from) { $pic = $store->get_value($position, 1); $minutes = round(abs(time() - strtotime($from)) / 60); $name = $this->temp . md5($pic); if (isset($this->pic_queue[$name])) { switch(true) { return; case ($minutes == 0): } elseif (isset($this->pic_cached[$name])) { return 'less than 1 minute ago'; $store = $this->treeview->get_model(); case ($minutes < 1): if (is_null($store->get_value($position, 0))) { $pixbuf = GdkPixbuf::new_from_file($name . '.jpg'); return '1 minute ago'; $store->set($position, 0, $pixbuf); case ($minutes <= 55): $cell->set_property('pixbuf', $pixbuf); return $minutes . ' minutes ago'; } case ($minutes <= 65): return; return 'about 1 hour ago'; } case ($minutes <= 1439): $this->pic_queue[$name] = array('name' => $name, 'url' => $pic, return 'about ' . round((float) $minutes / 60.0) . ' hours'; 'pos' => $position, 'cell' => $cell); case ($minutes <= 2879): if (empty($this->load_images_timeout)) { $this->load_images_timeout = Gtk::timeout_add(500, return '1 day ago'; default: array($this, 'pic_queue')); } return 'about ' . round((float) $minutes / 1440) . ' days ago'; } } } public function pic_queue() { $pic = array_shift($this->pic_queue); public function minimize_to_tray($window, $event) { if (empty($pic)) { if ($event->changed_mask == Gdk::WINDOW_STATE_ICONIFIED && $this->load_images_timeout = null; $event->new_window_state & Gdk::WINDOW_STATE_ICONIFIED) { return true; $window->hide(); } if (!file_exists($pic['name'])) { } file_put_contents($pic['name'] . '.jpg', return true; //stop bubbling file_get_contents($pic['url'])); } $this->pic_cached[$pic['name']] = $pic['url']; } public function update_public_timeline() { return true; // keep the timeout going $this->pic_queue = array(); } $list = $this->twitter->get_public_timeline(); $this->statusbar->pop(1); public function message_markup($column, $cell, $store, $this->statusbar->push(1, 'last updated ' . $position) { date('Y-m-d H:i') . ' - ' . count($list) . ' new tweets'); $user = utf8_decode($store->get_value($position, 2)); $store = $this->treeview->get_model(); $message = utf8_decode($store->get_value($position, 4)); $store->clear(); $time = $this->distance($store->get_value($position, 6)); foreach($list as $object) { $message = htmlspecialchars_decode($message, ENT_QUOTES); $store->append(array(null, $object->user->profile_image_url, $message = str_replace(array('@' . $user, '&nbsp;', '&'), array( $object->user->name, $object->user->id, $object->text, '<span foreground="#FF6633">@' $object->favorited, $object->created_at, . $user . '</span>', ' ', '&amp;'), $message); $object->id)); $cell->set_property('markup', } "<b>$user</b>:n$messagen<small>$time</small>"); return true; } }
  • 32. COMPLETED CODE Final Code, all 492 lines public function update_timeline() { public function logout() { $list = $this->twitter->get_timeline(); $this->statusbar->pop(1); $this->twitter->logout(); $this->statusbar->push(1, 'last updated ' . date('Y-m-d H:i') . ' - ' $this->logoutbutton->set_sensitive(false); . count($list) . ' new tweets'); $this->loginbutton->set_sensitive(true); $store = $this->treeview->get_model(); foreach($list as $object) { $this->public_timeline_timeout = Gtk::timeout_add(61000, array($this, $store->append(array(null, $object->user->profile_image_url, 'update_public_timeline')); // every 60 seconds $object->user->name, $object->user->id, $object->text, $this->pic_queue = array(); $object->favorited, $object->created_at, $this->pic_cached = array(); $object->id)); } $this->update_public_timeline(); return true; $this->updateentry->set_sensitive(false); } } public function login() { if (!empty($this->load_images_timeout)) { public function send_update($entry) { Gtk::timeout_remove($this->load_images_timeout); $readd = true; if ($this->twitter->send($entry->get_text())) { } $this->entrystatus->set_text('Message Sent'); Gtk::timeout_remove($this->public_timeline_timeout); $this->update_timeline(); $login = new Php_Gtk_Twitter_Login_Dialog($this); while($response = $login->run()) { $this->updateentry->set_text(''); if ($response == GTK::RESPONSE_CANCEL || } else { $response == GTK::RESPONSE_DELETE_EVENT) { $this->entrystatus->set_markup('<span color="red"> if (isset($readd)) { $this->load_images_timeout = Gtk::timeout_add(500, Error Sending Message - Try Again</span>'); array($this, 'pic_queue')); } } } $this->public_timeline_timeout = Gtk::timeout_add(61000, array($this, 'update_public_timeline')); // every 60 seconds $login->destroy(); public function __destruct() { break; foreach(scandir($this->temp) as $filename) { } elseif ($response == GTK::RESPONSE_OK) { if ($filename[0] == '.') if($login->check_login($this->twitter)) { $this->logoutbutton->set_sensitive(true); continue; $this->loginbutton->set_sensitive(false); if (file_exists($this->temp . $filename)) { $login->destroy(); $this->public_timeline_timeout = Gtk::timeout_add(61000, unlink($this->temp . $filename); array($this, 'update_timeline')); // every 60 seconds } $this->load_images_timeout = Gtk::timeout_add(500, } array($this, 'pic_queue')); } $this->treeview->get_model()->clear(); $this->pic_queue = array(); } $this->pic_cached = array(); $window = new Php_Gtk_Twitter_Client; $this->update_timeline(); $window->show_all(); $this->updateentry->set_sensitive(true); break; Gtk::main(); } } } }
  • 33. RESOURCES Wrapping it up • Slides • http://callicore.net/php-gtk/php-on-the-desktop.pdf • Code • http://callicore.net/php-gtk/php-gtk-twitter.zip • GTK docs • http://gtk.org • http://pygtk.org • http://gtk.php.net/docs.php • http://leonpegg.com/php-gtk-doc/phpweb/ • http://kksou.com • http://oops.opsat.net • http://php-gtk.eu • Me • http://elizabethmariesmith.com • http://callicore.net • http://callicore.net/php-gtk • [email protected] THANKS

Editor's Notes

  • #3: Tell the starting PHP story, tell the starting C story, blame it all on Andrei
  • #4: Ah, the big argument approaches
  • #5: People forget reasons why you’d want to have a desktop application
  • #6: Using PHP has some great advantages
  • #7: Speed is the big reason to pick C or something similar – stacked against ruby or python though, that’s the only advantage ;)
  • #8: Your computer does not care about HTML.Your shell does not care about HTML.PHP does not care about HTML.Because of all of this, you should not care about HTML.PHP and GTK does not use HTML.PHP and GTK does not build web pages.PHP and GTK does not build web applications.PHP and GTK builds computer programs.- From bob Majdak
  • #9: PHP-QT and Win::Gui are in active development if you aren’t worried about cross platformness – wxwidgets appears to be stalled, winbinder is stalled and doesn’t support latest php versions and is functional and is not thread safe (ouch ouch)
  • #10: PHP-QT and Win::Gui are in active development if you aren’t worried about cross platformness – wxwidgets appears to be stalled, winbinder is stalled and doesn’t support latest php versions and is functional and is not thread safe (ouch ouch)
  • #11: Tutorial Ahoy
  • #12: Basic concepts you need to use GTKThe main loop is like locking into a while loop and scripts run continuouslySignals are things that happen, and can have default callbacks or callbacks attached to themGtk is based around the concept of widgets – an item that is used for a specific purposePHP-GTK2 requires PHP OOP knowledgeItems are hidden on creation and can be shown and hidden recursively (using show_all and hide_all) – you can also block a specific widget from being included in an “all” call
  • #13: Using what we just talked aboutLet’s start with a basicgtk window – notice it’s easier to extend the base classConnect the destruction of our main window with stopping theConcepts – signals and connections, the main loop and main_quit, show allNotice if you put PHP after the main(); call it will not be executed until after the main_quit call
  • #14: Introduce a rather specialized widget – gtkstatusiconEverything is a gobject, hierarchy goes from there – most things are gtkwidgets for gtk itselfStock icons are cool and easy – callbacks stop bubbling when you return “true”, timeouts can be used to do something on a regular basis
  • #15: Quick look at stock items on windows – notice the pretty tango default themeThis is a demo from php-gtk2 in the /demos folder
  • #17: You can use php to do the data manipulationTreeviews show hierarchical data – the store or model decides if there are children or notThere are all kinds of renderers – some day we’ll have custom ones ;)
  • #20: First version of the app with the public timeline
  • #21: We begin with packing – do the pixel perfect and resize issue – show how packing is like packing with some hard and some soft containers
  • #22: Concepts: packing and toolbarsMore gtktimeout
  • #23: General concept of dialogs – when is a good time for modal dialogs, when is not a good time
  • #24: Concepts: packing and toolbarsMore gtktimeout
  • #25: Concepts: packing and toolbarsMore gtktimeout
  • #26: Add data in with a gtkentry
  • #27: Concepts: packing and toolbarsMore gtktimeout
  • #28: Here’s the finished app – then I’ll run it in action
  • #29: Twitter API code – just love wheel reinventing – don’t you?
  • #30: Dialog and Statusicon classes
  • #31: Humongo constructor
  • #32: Callbacks part 1 – treeview callbacks for formatting, the minimize override, and the public timeline updater
  • #33: Callbacks part 2, the login and login, update own timeline, and send – also destructor cleanup to not fill up temp and the bootstrap
  • #34: Places to get more information