[the-board/cheese: 7/10] [things] Webcam support in photo elements



commit e9b4adcc32f6520b28b918d29b0062c07ac4d3e8
Author: Lucas Rocha <lucasr gnome org>
Date:   Wed Mar 30 01:48:52 2011 +0100

    [things] Webcam support in photo elements
    
    https://bugzilla.gnome.org/show_bug.cgi?id=636625

 configure.ac                         |    3 +-
 data/things/photo/caption-border.png |  Bin 0 -> 219 bytes
 data/things/photo/style.css          |   14 ++-
 src/js/ui/things/photo.js            |  333 +++++++++++++++++++++++++++++++---
 4 files changed, 325 insertions(+), 25 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index f970443..99d1425 100644
--- a/configure.ac
+++ b/configure.ac
@@ -70,7 +70,8 @@ PKG_CHECK_MODULES(THE_BOARD,
                   mx-1.0 >= $MX_MIN_VERSION
                   gtk+-3.0 >= $GTK_MIN_VERSION
                   clutter-gtk-1.0 >= $CLUTTER_GTK_MIN_VERSION
-                  clutter-gst-1.0 >= $CLUTTER_GST_MIN_VERSION)
+                  clutter-gst-1.0 >= $CLUTTER_GST_MIN_VERSION
+                  cheese >= $CHEESE_MIN_VERSION)
 
 PKG_CHECK_MODULES(TB,
                   glib-2.0 >= $GLIB_MIN_VERSION
diff --git a/data/things/photo/caption-border.png b/data/things/photo/caption-border.png
new file mode 100644
index 0000000..a80cd14
Binary files /dev/null and b/data/things/photo/caption-border.png differ
diff --git a/data/things/photo/style.css b/data/things/photo/style.css
index 33be058..c357f47 100644
--- a/data/things/photo/style.css
+++ b/data/things/photo/style.css
@@ -3,12 +3,24 @@ MxWidget {
 }
 
 TbBox#photo-thing-photo-box {
-    spacing: 10px;
+    spacing: 5px;
     padding: 10px 10px 5px 10px;
     background-color: white;
 }
 
+MxLabel#photo-thing-camera-label,
 MxLabel#photo-thing-caption-label {
     color: black;
     font-size: 13;
+    padding: 5px 5px 2px 5px;
+    background-color: none;
+}
+
+MxLabel#photo-thing-camera-label {
+    border-image: url("caption-border.png") 3 3 3 3;
+    background-color: #F6E88A;
+}
+
+TbBox#photo-thing-camera-box {
+    background-color: black;
 }
diff --git a/src/js/ui/things/photo.js b/src/js/ui/things/photo.js
index d841453..edfe071 100644
--- a/src/js/ui/things/photo.js
+++ b/src/js/ui/things/photo.js
@@ -2,11 +2,14 @@
 const Gettext = imports.gettext.domain("the-board");
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
+const Signals = imports.signals;
 const Tweener = imports.tweener.tweener;
 
 // gi imports
+const Cheese = imports.gi.Cheese;
 const Clutter = imports.gi.Clutter;
 const GLib = imports.gi.GLib;
+const Gst = imports.gi.Gst;
 const Gtk = imports.gi.Gtk;
 const Mx = imports.gi.Mx;
 const Pango = imports.gi.Pango;
@@ -29,6 +32,179 @@ const _SHOW_BUTTON_BOX_TIME = 0.5;
 const _UPDATE_SIZE_TIME = 0.3;
 const _UPDATE_SIZE_TRANSITION = 'easeOutCubic';
 
+const _CAMERA_SPINNER_SIZE = 20;
+
+const _CAMERA_X_RESOLUTION = 640;
+const _CAMERA_Y_RESOLUTION = 480;
+
+const _CAPTION_LABEL_NAME_NORMAL = "photo-thing-caption-label";
+const _CAPTION_LABEL_NAME_CAMERA = "photo-thing-camera-label";
+
+const _PHOTO_TAKING_COUNTDOWN = 3; // in seconds
+
+function Camera(args) {
+    this._init(args);
+}
+
+Camera.prototype = {
+    _init : function(args) {
+        this._style = args.style;
+        this._photoTakingCountdown = -1;
+
+        this._createMainBox();
+        this._createVideoTexture();
+        this._createSpinner();
+        this._createCamera();
+    },
+
+    _createMainBox : function() {
+        this._mainBox =
+            new TheBoard.Box({ orientation: TheBoard.BoxOrientation.VERTICAL,
+                               xAlign: TheBoard.BoxAlignment.FILL,
+                               yAlign: TheBoard.BoxAlignment.FILL,
+                               style: this._style,
+                               name: "photo-thing-camera-box" });
+    },
+
+    _createVideoTexture : function() {
+        this._videoTexture =
+            new Clutter.Texture({ keepAspectRatio: true });
+
+        this._mainBox.append(this._videoTexture,
+                             TheBoard.BoxPackFlags.EXPAND);
+    },
+
+    _createSpinner : function() {
+        this._spinner =
+            new Mx.Spinner({ width: _CAMERA_SPINNER_SIZE,
+                             height: _CAMERA_SPINNER_SIZE,
+                             name: "photo-thing-camera-spinner" });
+
+        this._mainBox.append(this._spinner,
+                             TheBoard.BoxPackFlags.FIXED);
+
+        this._mainBox.set_fixed_child_align(this._spinner,
+                                            TheBoard.BoxAlignment.CENTER,
+                                            TheBoard.BoxAlignment.CENTER);
+    },
+
+    _createCamera : function() {
+        // We use FileUtil.new() here because FileUtil
+        // is a supposed to be a singleton which is returned
+        // from cheese_fileutil_new()
+        let fileUtil = Cheese.FileUtil.new();
+
+        this._filename =
+            fileUtil.get_new_media_filename(Cheese.MediaMode.PHOTO);
+
+        this._camera = Cheese.Camera.new(this._videoTexture,
+                                         null,
+                                         _CAMERA_X_RESOLUTION,
+                                         _CAMERA_Y_RESOLUTION);
+
+        this._camera.connect("state-flags-changed",
+                             Lang.bind(this,
+                                       this._onCameraStateChanged));
+
+        this._camera.connect("photo-saved",
+                             Lang.bind(this,
+                                       this._onCameraPhotoSaved));
+
+        TheBoard.cheese_camera_start_async(this._camera,
+                                           Lang.bind(this,
+                                                     this._onCameraStartAsync));
+    },
+
+    _onCameraStartAsync : function(camera, result, data) {
+        try {
+            TheBoard.cheese_camera_start_finish(camera, result);
+        } catch(e) {
+            // FIXME: Show error message to user
+        }
+    },
+
+    _onCameraStateChanged : function(camera, gstState) {
+        if (gstState != Gst.State.PLAYING) {
+            return;
+        }
+
+        this._spinner.hide();
+
+        this._photoTakingCountdown = _PHOTO_TAKING_COUNTDOWN;
+
+        this.emit("caption-changed");
+
+        this._photoTakingTimeoutId =
+            Mainloop.timeout_add_seconds(1,
+                                         Lang.bind(this,
+                                                   this._onPhotoTakingTimeout));
+    },
+
+    _onCameraPhotoSaved : function(camera, filename) {
+        // We have to emit this signal on idle here
+        // because apparently libcheese doesn't handle
+        // very well the case where we stop camera in
+        // response to "photo-saved" signal.
+        let emitPhotoSaved = function() {
+            this.emit("photo-saved");
+        };
+
+        Mainloop.idle_add(Lang.bind(this, emitPhotoSaved));
+    },
+
+    _onPhotoTakingTimeout : function(filename) {
+        this._photoTakingCountdown--;
+
+        // We want to give one more second for the
+        // "Say cheese" message just before actually
+        // taking the photo
+        if (this._photoTakingCountdown < 0) {
+            this._camera.take_photo(this._filename);
+        } else {
+            this.emit("caption-changed");
+        }
+
+        return (this._photoTakingCountdown >= 0);
+    },
+
+    destroy : function() {
+        if (this._photoTakingTimeoutId) {
+            Mainloop.source_remove(this._photoTakingTimeoutId);
+            delete this._photoTakingTimeoutId;
+        }
+
+        if (this._camera) {
+            this._camera.stop();
+            delete this._camera;
+        }
+
+        if (this._mainBox) {
+            this._mainBox.destroy();
+            delete this._mainBox;
+        }
+    },
+
+    get caption() {
+        if (this._photoTakingCountdown == 0) {
+            return Gettext.gettext("Say cheese!");
+        } else if (this._photoTakingCountdown > 0) {
+            return "" + this._photoTakingCountdown;
+        } else {
+            return Gettext.gettext("Starting webcam");
+        }
+    },
+
+    get filename() {
+        return this._filename;
+    },
+
+    get actor() {
+        return this._mainBox;
+    }
+}
+
+Signals.addSignalMethods(Camera.prototype);
+
 function PhotoThing(args) {
     this._init(args);
 }
@@ -109,7 +285,7 @@ PhotoThing.prototype = {
         this._captionLabel =
             new Mx.Label({ xAlign: Mx.Align.MIDDLE,
                            style: this._style,
-                           name: "photo-thing-caption-label" });
+                           name: _CAPTION_LABEL_NAME_NORMAL });
 
         this._captionLabel.clutterText.lineAlignment = Pango.Alignment.CENTER;
 
@@ -278,38 +454,65 @@ PhotoThing.prototype = {
         this._captionLabel.clutterText.editable = false;
     },
 
-    _onCaptionTextKeyPressEvent : function(o, event) {
-        let key = event.get_key_symbol();
-
-        switch (key) {
-        case Clutter.Return:
-            this._updatePhotoWithFileChooser();
-            return true;
-        case Clutter.Escape:
-            this.emit("deactivate");
-            return true;
+    _startTakingPhoto : function() {
+        if (this._camera) {
+            return;
         }
 
-        return false;
-    },
+        this._updateInitialSize(_CAMERA_X_RESOLUTION,
+                                _CAMERA_Y_RESOLUTION);
 
-    _onCaptionTextChanged : function() {
-        this.emit('save');
+        this._animateToPhotoSize(false /* not from state */);
+
+        this._camera = new Camera({ style: this._style });
+
+        this._cameraPhotoTakenId =
+            this._camera.connect("photo-saved",
+                                 Lang.bind(this, this._onPhotoSavedFromCamera));
+
+        this._cameraCaptionChangedId =
+            this._camera.connect("caption-changed",
+                                 Lang.bind(this, this._onCameraCaptionChanged));
+
+        this._contentBox.append(this._camera.actor,
+                                TheBoard.BoxPackFlags.FIXED);
+
+        this._contentBox.set_fixed_child_align(this._camera.actor,
+                                               TheBoard.BoxAlignment.FILL,
+                                               TheBoard.BoxAlignment.FILL);
+
+        this._textBeforeTakingPhoto = this._captionLabel.text;
+
+        this._captionLabel.name = _CAPTION_LABEL_NAME_CAMERA;
+        this._updateCaptionFromCamera();
+        this._stopEditingCaption();
     },
 
-    _onPhotoSizeChange : function(photo, width, height) {
-        if (this._photoWidth > 0 && this._photoHeight > 0) {
+    _stopTakingPhoto : function() {
+        if (!this._camera) {
             return;
         }
 
-        this._updateInitialSize(width, height);
-    },
+        if (this._cameraPhotoTakenId) {
+            this._camera.disconnect(this._cameraPhotoTakenId);
+            delete this._cameraPhotoTakenId;
+        }
 
-    _onPhotoLoadFinished : function(texture, error, fromState) {
-        this._disconnectPhotoSignals();
-        this._updateSpinner();
+        if (this._cameraCaptionChangedId) {
+            this._camera.disconnect(this._cameraCaptionChangedId);
+            delete this._cameraCaptionChangedId;
+        }
+
+        this._camera.destroy();
+        delete this._camera;
+
+        this._captionLabel.name = _CAPTION_LABEL_NAME_NORMAL;
+        this._captionLabel.text = this._textBeforeTakingPhoto;
+        delete this._textBeforeTakingPhoto;
+    },
 
-        [minTextHeight, naturalTextHeight] =
+    _animateToPhotoSize : function(fromState) {
+        let [minTextHeight, naturalTextHeight] =
             this._captionLabel.get_preferred_height(-1);
 
         let thingWidth = this._photoWidth;
@@ -335,12 +538,90 @@ PhotoThing.prototype = {
         delete this._photoHeight;
     },
 
+    _updateCaptionFromCamera : function() {
+        this._captionLabel.text = this._camera.caption;
+    },
+
+    _maybeSetDateTimeCaption : function(textBeforeTakingPhoto) {
+        if (textBeforeTakingPhoto) {
+            return;
+        }
+
+        let now = new Date();
+
+        this._captionLabel.text =
+            now.toLocaleDateString() + " " + now.toLocaleTimeString();
+    },
+
+    _onPhotoSavedFromCamera : function(camera) {
+        // Copy previous caption value before calling
+        // _stopTakingPhoto() because it will clear and
+        // possibly restore this value in captionLabel
+        let textBeforeTakingPhoto = this._textBeforeTakingPhoto;
+
+        this._stopTakingPhoto();
+
+        // This will set captionLabel value to a date
+        // in case a photo was taken while no caption had
+        // been defined
+        this._maybeSetDateTimeCaption(textBeforeTakingPhoto);
+
+        this._startEditingCaption();
+
+        this._updateImageFilename(camera.filename,
+                                  false /* not from state */);
+    },
+
+    _onCameraCaptionChanged : function(camera) {
+        this._updateCaptionFromCamera();
+    },
+
+    _onCaptionTextKeyPressEvent : function(o, event) {
+        let key = event.get_key_symbol();
+
+        switch (key) {
+        case Clutter.Return:
+            this._updatePhotoWithFileChooser();
+            return true;
+        case Clutter.Escape:
+            this.emit("deactivate");
+            return true;
+        }
+
+        return false;
+    },
+
+    _onCaptionTextChanged : function() {
+        // Don't trigger save when we set the label
+        // text as part of the photo taking countdown
+        if (this._camera) {
+            return;
+        }
+
+        this.emit('save');
+    },
+
+    _onPhotoSizeChange : function(photo, width, height) {
+        if (this._photoWidth > 0 && this._photoHeight > 0) {
+            return;
+        }
+
+        this._updateInitialSize(width, height);
+    },
+
+    _onPhotoLoadFinished : function(texture, error, fromState) {
+        this._disconnectPhotoSignals();
+        this._updateSpinner();
+        this._animateToPhotoSize(fromState);
+    },
+
     activate : function() {
         this._startEditingCaption();
     },
 
     deactivate : function() {
         this._stopEditingCaption();
+        this._stopTakingPhoto();
     },
 
     loadState : function(state) {
@@ -365,6 +646,8 @@ PhotoThing.prototype = {
     doAction : function(actionName, actionArgs) {
         if (actionName == "chooseFile") {
             this._updatePhotoWithFileChooser();
+        } else if (actionName == "takePhoto") {
+            this._startTakingPhoto();
         }
     },
 
@@ -384,6 +667,7 @@ PhotoThing.prototype = {
 
     destroy : function() {
         this._disconnectPhotoSignals();
+        this._stopTakingPhoto();
 
         if (this._photoBox) {
             this._photoBox.destroy();
@@ -427,6 +711,9 @@ function createToolbar(args) {
     toolBox.addButton({ label: Gettext.gettext("File"),
                         actionName: "chooseFile" });
 
+    toolBox.addButton({ label: Gettext.gettext("Webcam"),
+                        actionName: "takePhoto" });
+
     toolbar.addToolBox(toolBox);
 
     return toolbar;



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]