[gnome-documents] fullscreen: add a GtkClutter fading toolbar in fullscreen mode



commit 91cd6b29812b45cbf9a6d3c317277104f93da5a5
Author: Cosimo Cecchi <cosimoc gnome org>
Date:   Tue Aug 30 22:29:02 2011 -0400

    fullscreen: add a GtkClutter fading toolbar in fullscreen mode
    
    This makes Documents use GtkClutter instead of simple Gtk, but we can do
    more fancy visuals this way.

 configure.ac        |    2 +
 src/Makefile-js.am  |    4 +
 src/application.js  |    5 +-
 src/mainToolbar.js  |   13 ++-
 src/mainWindow.js   |   79 ++++++++++++++-
 src/util/tweener.js |  273 +++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 367 insertions(+), 9 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index ccb14fc..ff5becf 100644
--- a/configure.ac
+++ b/configure.ac
@@ -54,8 +54,10 @@ GTK_MIN_VERSION=3.1.13
 GOBJECT_INTROSPECTION_MIN_VERSION=0.9.6
 GDATA_MIN_VERSION=0.9.1
 GOA_MIN_VERSION=3.1.90
+CLUTTER_GTK_MIN_VERSION=1.0.1
 
 PKG_CHECK_MODULES(DOCUMENTS,
+                  clutter-gtk-1.0 >= $CLUTTER_GTK_MIN_VERSION
                   evince-document-3.0
                   evince-view-3.0
                   glib-2.0 >= $GLIB_MIN_VERSION
diff --git a/src/Makefile-js.am b/src/Makefile-js.am
index 008038f..643452f 100644
--- a/src/Makefile-js.am
+++ b/src/Makefile-js.am
@@ -29,6 +29,10 @@ dist_js_DATA = \
     format.js \
     path.js
 
+jsutildir = $(pkgdatadir)/js/util/
+dist_jsutil_DATA = \
+    util/tweener.js
+
 BUILT_SOURCES += path.js
 
 path.js: Makefile path.js.in
diff --git a/src/application.js b/src/application.js
index 0eb50dd..0a1af62 100644
--- a/src/application.js
+++ b/src/application.js
@@ -23,6 +23,7 @@ const DBus = imports.dbus;
 const Lang = imports.lang;
 const Gettext = imports.gettext;
 
+const GtkClutter = imports.gi.GtkClutter;
 const EvDoc = imports.gi.EvinceDocument;
 const Gdk = imports.gi.Gdk;
 const Gio = imports.gi.Gio;
@@ -45,6 +46,7 @@ const Query = imports.query;
 const SelectionController = imports.selectionController;
 const Sources = imports.sources;
 const TrackerController = imports.trackerController;
+const Tweener = imports.util.tweener;
 
 const _GD_DBUS_PATH = '/org/gnome/Documents';
 
@@ -106,8 +108,9 @@ Application.prototype = {
         GLib.setenv('TRACKER_SPARQL_BACKEND', 'bus', true);
 
         GLib.set_prgname('gnome-documents');
-        Gtk.init(null, null);
+        GtkClutter.init(null, null);
         EvDoc.init();
+        Tweener.init();
 
         let provider = new Gtk.CssProvider();
         provider.load_from_path(Path.STYLE_DIR + "gtk-style.css");
diff --git a/src/mainToolbar.js b/src/mainToolbar.js
index b679930..c3f6e28 100644
--- a/src/mainToolbar.js
+++ b/src/mainToolbar.js
@@ -33,15 +33,20 @@ const MainWindow = imports.mainWindow;
 
 const _SEARCH_ENTRY_TIMEOUT = 200;
 
-function MainToolbar() {
-    this._init();
+function MainToolbar(windowMode) {
+    this._init(windowMode);
 }
 
 MainToolbar.prototype = {
-    _init: function() {
+    _init: function(windowMode) {
+        this._model = null;
+        this._document = null;
         this._searchEntryTimeout = 0;
+
         this.widget = new Gtk.Toolbar({ icon_size: Gtk.IconSize.MENU });
         this.widget.get_style_context().add_class(Gtk.STYLE_CLASS_MENUBAR);
+
+        this.setWindowMode(windowMode);
     },
 
     _clearToolbar: function() {
@@ -162,7 +167,7 @@ MainToolbar.prototype = {
 
         if (windowMode == MainWindow.WindowMode.OVERVIEW)
             this._populateForOverview();
-        else
+        else if (windowMode == MainWindow.WindowMode.PREVIEW)
             this._populateForPreview();
     },
 
diff --git a/src/mainWindow.js b/src/mainWindow.js
index ee3812e..0c2c533 100644
--- a/src/mainWindow.js
+++ b/src/mainWindow.js
@@ -19,6 +19,7 @@
  *
  */
 
+const Clutter = imports.gi.Clutter;
 const EvView = imports.gi.EvinceView;
 const Gd = imports.gi.Gd;
 const Gdk = imports.gi.Gdk;
@@ -26,6 +27,7 @@ const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
 const Gtk = imports.gi.Gtk;
+const GtkClutter = imports.gi.GtkClutter;
 
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
@@ -40,6 +42,7 @@ const ListView = imports.listView;
 const Preview = imports.preview;
 const SpinnerBox = imports.spinnerBox;
 const TrackerUtils = imports.trackerUtils;
+const Tweener = imports.util.tweener;
 
 const _ = imports.gettext.gettext;
 
@@ -48,6 +51,8 @@ const _WINDOW_DEFAULT_HEIGHT = 600;
 
 const _PDF_LOADER_TIMEOUT = 300;
 
+const _FULLSCREEN_TOOLBAR_TIMEOUT = 2;
+
 const WindowMode = {
     NONE: 0,
     OVERVIEW: 1,
@@ -61,6 +66,8 @@ function MainWindow() {
 MainWindow.prototype = {
     _init: function() {
         this._adjChangedId = 0;
+        this._docModel = null;
+        this._document = null;
         this._pdfLoader = null;
         this._fullscreen = false;
         this._loaderCancellable = null;
@@ -69,7 +76,7 @@ MainWindow.prototype = {
         this._scrolledWindowId = 0;
         this._windowMode = WindowMode.NONE;
 
-        this.window = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL,
+        this.window = new GtkClutter.Window({ type: Gtk.WindowType.TOPLEVEL,
                                        window_position: Gtk.WindowPosition.CENTER,
                                        title: _("Documents") });
 
@@ -96,7 +103,7 @@ MainWindow.prototype = {
         this._viewContainer = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL });
         this._grid.add(this._viewContainer);
 
-        this._toolbar = new MainToolbar.MainToolbar();
+        this._toolbar = new MainToolbar.MainToolbar(this._windowMode);
         this._toolbar.connect('back-clicked',
                               Lang.bind(this, this._onToolbarBackClicked));
         this._viewContainer.add(this._toolbar.widget);
@@ -216,6 +223,9 @@ MainWindow.prototype = {
             this._preview = null;
         }
 
+        this._docModel = null;
+        this._document = null;
+
         this._setFullscreen(false);
 
         this._refreshViewSettings();
@@ -227,18 +237,43 @@ MainWindow.prototype = {
         this._sidebar.widget.show();
     },
 
+    _createFullscreenToolbar: function() {
+        this._fsToolbar = new MainToolbar.MainToolbar(this._windowMode);
+        this._fsToolbar.setModel(this._docModel, this._document);
+
+        this._fsToolbar.connect('back-clicked',
+                                Lang.bind(this, this._onToolbarBackClicked));
+
+        this._fsToolbarActor = new GtkClutter.Actor({ contents: this._fsToolbar.widget,
+                                                      opacity: 0 });
+        this._fsToolbarActor.add_constraint(
+            new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.WIDTH,
+                                         source: this.window.get_stage(),
+                                         offset: - (this._scrolledWin.get_vscrollbar().get_preferred_width()[1]) }));
+        this.window.get_stage().add_actor(this._fsToolbarActor);
+    },
+
+    _destroyFullscreenToolbar: function() {
+        this._fsToolbar.widget.destroy();
+    },
+
     _setFullscreen: function(fullscreen) {
         if (this._fullscreen == fullscreen)
             return;
 
         this._fullscreen = fullscreen;
+        this._motionTimeoutId = 0;
 
         Gtk.Settings.get_default().gtk_application_prefer_dark_theme = this._fullscreen;
+        this._toolbar.widget.visible = !this._fullscreen;
 
-        if (this._fullscreen)
+        if (this._fullscreen) {
             this.window.fullscreen();
-        else
+            this._createFullscreenToolbar();
+        } else {
+            this._destroyFullscreenToolbar();
             this.window.unfullscreen();
+        }
     },
 
     _onDeleteEvent: function() {
@@ -284,8 +319,13 @@ MainWindow.prototype = {
         this._preview = new Preview.PreviewView(model, document);
         this._toolbar.setModel(model, document);
 
+        this._docModel = model;
+        this._document = document;
+
         this._preview.widget.connect('button-press-event',
                                      Lang.bind(this, this._onPreviewButtonPressEvent));
+        this._preview.widget.connect('motion-notify-event',
+                                     Lang.bind(this, this._onPreviewMotionNotifyEvent));
 
         this._scrolledWin.add(this._preview.widget);
     },
@@ -302,6 +342,37 @@ MainWindow.prototype = {
         return false;
     },
 
+    _onPreviewMotionNotifyEvent: function(widget, event) {
+        if (!this._fullscreen)
+            return false;
+
+        // if we were idle fade in the toolbar, otherwise reset
+        // the timeout
+        if (this._motionTimeoutId == 0) {
+                Tweener.addTween(this._fsToolbarActor,
+                                 { opacity: 255,
+                                   time: 0.20,
+                                   transition: 'easeOutQuad' });
+        } else {
+            Mainloop.source_remove(this._motionTimeoutId);
+        }
+
+        this._motionTimeoutId = Mainloop.timeout_add_seconds
+            (_FULLSCREEN_TOOLBAR_TIMEOUT, Lang.bind(this,
+                function() {
+                    this._motionTimeoutId = 0;
+
+                    // fade out the toolbar
+                    Tweener.addTween(this._fsToolbarActor,
+                                     { opacity: 0,
+                                       time: 0.20,
+                                       transition: 'easeOutQuad' });
+                return false;
+            }));
+
+        return false;
+    },
+
     _onToolbarBackClicked: function() {
         this._setWindowMode(WindowMode.OVERVIEW);
     },
diff --git a/src/util/tweener.js b/src/util/tweener.js
new file mode 100644
index 0000000..32a149b
--- /dev/null
+++ b/src/util/tweener.js
@@ -0,0 +1,273 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+/*
+ * Copyright (C) 2009-2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ *
+ * Authors: Dan Winship <danw gnome org>
+ *
+ */
+
+const Clutter = imports.gi.Clutter;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Tweener = imports.tweener.tweener;
+const Signals = imports.signals;
+
+// This was imported from gnome-shell and stipped of shell-specific stuff
+
+// This is a wrapper around imports.tweener.tweener that adds a bit of
+// Clutter integration and some additional callbacks:
+//
+//   1. If the tweening target is a Clutter.Actor, then the tweenings
+//      will automatically be removed if the actor is destroyed
+//
+//   2. If target._delegate.onAnimationStart() exists, it will be
+//      called when the target starts being animated.
+//
+//   3. If target._delegate.onAnimationComplete() exists, it will be
+//      called once the target is no longer being animated.
+//
+// The onAnimationStart() and onAnimationComplete() callbacks differ
+// from the tweener onStart and onComplete parameters, in that (1)
+// they track whether or not the target has *any* tweens attached to
+// it, as opposed to be called for *each* tween, and (2)
+// onAnimationComplete() is always called when the object stops being
+// animated, regardless of whether it stopped normally or abnormally.
+//
+// onAnimationComplete() is called at idle time, which means that if a
+// tween completes and then another is added before returning to the
+// main loop, the complete callback will not be called (until the new
+// tween finishes).
+
+
+// ActionScript Tweener methods that imports.tweener.tweener doesn't
+// currently implement: getTweens, getVersion, registerTransition,
+// setTimeScale, updateTime.
+
+// imports.tweener.tweener methods that we don't re-export:
+// pauseAllTweens, removeAllTweens, resumeAllTweens. (It would be hard
+// to clean up properly after removeAllTweens, and also, any code that
+// calls any of these is almost certainly wrong anyway, because they
+// affect the entire application.)
+
+// Called from Main.start
+function init() {
+    Tweener.setFrameTicker(new ClutterFrameTicker());
+}
+
+
+function addCaller(target, tweeningParameters) {
+    _wrapTweening(target, tweeningParameters);
+    Tweener.addCaller(target, tweeningParameters);
+}
+
+function addTween(target, tweeningParameters) {
+    _wrapTweening(target, tweeningParameters);
+    Tweener.addTween(target, tweeningParameters);
+}
+
+function _wrapTweening(target, tweeningParameters) {
+    let state = _getTweenState(target);
+
+    if (!state.destroyedId) {
+        if (target instanceof Clutter.Actor) {
+            state.actor = target;
+            state.destroyedId = target.connect('destroy', _actorDestroyed);
+        } else if (target.actor && target.actor instanceof Clutter.Actor) {
+            state.actor = target.actor;
+            state.destroyedId = target.actor.connect('destroy', function() { _actorDestroyed(target); });
+        }
+    }
+
+    _addHandler(target, tweeningParameters, 'onStart', _tweenStarted);
+    _addHandler(target, tweeningParameters, 'onComplete', _tweenCompleted);
+}
+
+function _getTweenState(target) {
+    // If we were paranoid, we could keep a plist mapping targets to
+    // states... but we're not that paranoid.
+    if (!target.__ShellTweenerState)
+        _resetTweenState(target);
+    return target.__ShellTweenerState;
+}
+
+function _resetTweenState(target) {
+    let state = target.__ShellTweenerState;
+
+    if (state) {
+        if (state.destroyedId)
+            state.actor.disconnect(state.destroyedId);
+        if (state.idleCompletedId)
+            Mainloop.source_remove(state.idleCompletedId);
+    }
+
+    target.__ShellTweenerState = {};
+}
+
+function _addHandler(target, params, name, handler) {
+    if (params[name]) {
+        let oldHandler = params[name];
+        let oldScope = params[name + 'Scope'];
+        let oldParams = params[name + 'Params'];
+        let eventScope = oldScope ? oldScope : target;
+
+        params[name] = function () {
+            oldHandler.apply(eventScope, oldParams);
+            handler(target);
+        };
+    } else
+        params[name] = function () { handler(target); };
+}
+
+function _actorDestroyed(target) {
+    _resetTweenState(target);
+    Tweener.removeTweens(target);
+}
+
+function _tweenStarted(target) {
+    let state = _getTweenState(target);
+    let delegate = target._delegate;
+
+    if (!state.running && delegate && delegate.onAnimationStart)
+        delegate.onAnimationStart();
+    state.running = true;
+}
+
+function _tweenCompleted(target) {
+    let state = _getTweenState(target);
+
+    if (!state.idleCompletedId)
+        state.idleCompletedId = Mainloop.idle_add(Lang.bind(null, _idleCompleted, target));
+}
+
+function _idleCompleted(target) {
+    let state = _getTweenState(target);
+    let delegate = target._delegate;
+
+    if (!isTweening(target)) {
+        _resetTweenState(target);
+        if (delegate && delegate.onAnimationComplete)
+            delegate.onAnimationComplete();
+    }
+    return false;
+}
+
+function getTweenCount(scope) {
+    return Tweener.getTweenCount(scope);
+}
+
+// imports.tweener.tweener doesn't provide this method (which exists
+// in the ActionScript version) but it's easy to implement.
+function isTweening(scope) {
+    return Tweener.getTweenCount(scope) != 0;
+}
+
+function removeTweens(scope) {
+    if (Tweener.removeTweens.apply(null, arguments)) {
+        // If we just removed the last active tween, clean up
+        if (Tweener.getTweenCount(scope) == 0)
+            _tweenCompleted(scope);
+        return true;
+    } else
+        return false;
+}
+
+function pauseTweens() {
+    return Tweener.pauseTweens.apply(null, arguments);
+}
+
+function resumeTweens() {
+    return Tweener.resumeTweens.apply(null, arguments);
+}
+
+
+function registerSpecialProperty(name, getFunction, setFunction,
+                                 parameters, preProcessFunction) {
+    Tweener.registerSpecialProperty(name, getFunction, setFunction,
+                                    parameters, preProcessFunction);
+}
+
+function registerSpecialPropertyModifier(name, modifyFunction, getFunction) {
+    Tweener.registerSpecialPropertyModifier(name, modifyFunction, getFunction);
+}
+
+function registerSpecialPropertySplitter(name, splitFunction, parameters) {
+    Tweener.registerSpecialPropertySplitter(name, splitFunction, parameters);
+}
+
+
+// The 'FrameTicker' object is an object used to feed new frames to
+// Tweener so it can update values and redraw. The default frame
+// ticker for Tweener just uses a simple timeout at a fixed frame rate
+// and has no idea of "catching up" by dropping frames.
+//
+// We substitute it with custom frame ticker here that connects
+// Tweener to a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a
+// whole lot more sophisticated than a simple timeout at a fixed frame
+// rate, but at least it knows how to drop frames. (See
+// HippoAnimationManager for a more sophisticated view of continous
+// time updates; even better is to pay attention to the vertical
+// vblank and sync to that when possible.)
+//
+function ClutterFrameTicker() {
+    this._init();
+}
+
+ClutterFrameTicker.prototype = {
+    FRAME_RATE : 60,
+
+    _init : function() {
+        // We don't have a finite duration; tweener will tell us to stop
+        // when we need to stop, so use 1000 seconds as "infinity"
+        this._timeline = new Clutter.Timeline({ duration: 1000*1000 });
+        this._startTime = -1;
+
+        this._timeline.connect('new-frame', Lang.bind(this,
+            function(timeline, frame) {
+                this._onNewFrame(frame);
+            }));
+    },
+
+    _onNewFrame : function(frame) {
+        // If there is a lot of setup to start the animation, then
+        // first frame number we get from clutter might be a long ways
+        // into the animation (or the animation might even be done).
+        // That looks bad, so we always start at the first frame of the
+        // animation then only do frame dropping from there.
+        if (this._startTime < 0)
+            this._startTime = this._timeline.get_elapsed_time();
+
+        // currentTime is in milliseconds
+        this.emit('prepare-frame');
+    },
+
+    getTime : function() {
+        return this._timeline.get_elapsed_time();
+    },
+
+    start : function() {
+        this._timeline.start();
+    },
+
+    stop : function() {
+        this._timeline.stop();
+        this._startTime = -1;
+    }
+};
+
+Signals.addSignalMethods(ClutterFrameTicker.prototype);



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