[gnome-documents] fullscreen: add a GtkClutter fading toolbar in fullscreen mode
- From: Cosimo Cecchi <cosimoc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-documents] fullscreen: add a GtkClutter fading toolbar in fullscreen mode
- Date: Wed, 31 Aug 2011 16:29:24 +0000 (UTC)
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]