[gnome-maps] Add export to PNG function to context menu



commit 08ad2b76dc83d145fc05d7bd94ba4fcc303fdcfb
Author: Jonas Danielsson <jonas threetimestwo org>
Date:   Tue Nov 17 15:46:52 2015 +0100

    Add export to PNG function to context menu
    
    https://bugzilla.gnome.org/show_bug.cgi?id=753653

 data/org.gnome.Maps.data.gresource.xml |    1 +
 data/ui/context-menu.ui                |    7 ++
 data/ui/export-view-dialog.ui          |  127 +++++++++++++++++++++++
 src/contextMenu.js                     |   38 +++++++-
 src/exportViewDialog.js                |  174 ++++++++++++++++++++++++++++++++
 src/org.gnome.Maps.src.gresource.xml   |    1 +
 6 files changed, 347 insertions(+), 1 deletions(-)
---
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml
index 6ba6040..7e25b57 100644
--- a/data/org.gnome.Maps.data.gresource.xml
+++ b/data/org.gnome.Maps.data.gresource.xml
@@ -6,6 +6,7 @@
     <file preprocess="xml-stripblanks">ui/busy-marker.ui</file>
     <file preprocess="xml-stripblanks">ui/check-in-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/context-menu.ui</file>
+    <file preprocess="xml-stripblanks">ui/export-view-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/favorites-popover.ui</file>
     <file preprocess="xml-stripblanks">ui/instruction-row.ui</file>
     <file preprocess="xml-stripblanks">ui/layers-popover.ui</file>
diff --git a/data/ui/context-menu.ui b/data/ui/context-menu.ui
index 41220e6..9a5e868 100644
--- a/data/ui/context-menu.ui
+++ b/data/ui/context-menu.ui
@@ -17,5 +17,12 @@
         <property name="visible">True</property>
       </object>
     </child>
+    <child>
+      <object class="GtkMenuItem" id="exportItem">
+        <property name="name">export-item</property>
+        <property name="label" translatable="yes">Export to PNG</property>
+        <property name="visible">True</property>
+      </object>
+    </child>
   </template>
 </interface>
diff --git a/data/ui/export-view-dialog.ui b/data/ui/export-view-dialog.ui
new file mode 100644
index 0000000..092c718
--- /dev/null
+++ b/data/ui/export-view-dialog.ui
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_ExportViewDialog" parent="GtkDialog">
+    <property name="visible">False</property>
+    <property name="can_focus">False</property>
+    <property name="use_header_bar">1</property>
+    <child internal-child="headerbar">
+      <object class="GtkHeaderBar" id="headerBar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="show_close_button">False</property>
+        <property name="title" translatable="yes">Export view</property>
+        <style>
+          <class name="titlebar"/>
+        </style>
+        <child>
+          <object class="GtkButton" id="cancelButton">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="use_underline">True</property>
+            <style>
+              <class name="text-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="exportButton">
+            <property name="label" translatable="yes">_Export</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="has_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="valign">center</property>
+            <property name="use_underline">True</property>
+            <style>
+              <class name="suggested-action"/>
+              <class name="text-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="contentArea">
+        <child>
+          <object class="GtkGrid" id="grid">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_left">5</property>
+            <property name="margin_right">5</property>
+            <property name="margin_top">5</property>
+            <property name="margin_bottom">5</property>
+            <property name="row_spacing">8</property>
+            <property name="column_spacing">8</property>
+            <child>
+              <object class="GtkAlignment" id="alignment">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="vexpand">True</property>
+                <child>
+                  <object class="GtkFrame" id="frame">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label_xalign">0</property>
+                    <property name="shadow_type">out</property>
+                    <child>
+                      <object class="GtkDrawingArea" id="previewArea">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="filenameEntry">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="valign">start</property>
+                <property name="activates_default">True</property>
+                <property name="width_chars">32</property>
+              </object>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkFileChooserButton" id="fileChooserButton">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="valign">start</property>
+                <property name="vexpand">True</property>
+                <property name="action">select-folder</property>
+                <property name="local_only">False</property>
+              </object>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="top_attach">1</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/contextMenu.js b/src/contextMenu.js
index fd4381f..39633b1 100644
--- a/src/contextMenu.js
+++ b/src/contextMenu.js
@@ -19,6 +19,7 @@
  * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
  */
 
+const Champlain = imports.gi.Champlain;
 const Clutter = imports.gi.Clutter;
 const Gdk = imports.gi.Gdk;
 const Geocode = imports.gi.GeocodeGlib;
@@ -26,6 +27,7 @@ const Gtk = imports.gi.Gtk;
 const Mainloop = imports.mainloop;
 
 const Application = imports.application;
+const ExportViewDialog = imports.exportViewDialog;
 const Lang = imports.lang;
 const Location = imports.location;
 const Utils = imports.utils;
@@ -35,7 +37,8 @@ const ContextMenu = new Lang.Class({
     Extends: Gtk.Menu,
     Template: 'resource:///org/gnome/Maps/ui/context-menu.ui',
     InternalChildren: [ 'whatsHereItem',
-                        'geoURIItem' ],
+                        'geoURIItem',
+                        'exportItem' ],
 
     _init: function(params) {
         this._mapView = params.mapView;
@@ -50,6 +53,8 @@ const ContextMenu = new Lang.Class({
                                     this._onWhatsHereActivated.bind(this));
         this._geoURIItem.connect('activate',
                                  this._onGeoURIActivated.bind(this));
+        this._exportItem.connect('activate',
+                                 this._onExportActivated.bind(this));
     },
 
     _onButtonReleaseEvent: function(actor, event) {
@@ -85,5 +90,36 @@ const ContextMenu = new Lang.Class({
         let uri = location.to_uri(Geocode.LocationURIScheme.GEO);
 
         clipboard.set_text(uri, uri.length);
+    },
+
+    _activateExport: function() {
+        let view = this._mapView.view;
+        let surface = view.to_surface(true);
+        let bbox = view.get_bounding_box();
+        let [latitude, longitude] = bbox.get_center();
+
+        let dialog = new ExportViewDialog.ExportViewDialog({
+            transient_for: this.get_toplevel(),
+            surface: surface,
+            latitude: latitude,
+            longitude: longitude,
+            mapView: this._mapView
+        });
+
+        dialog.run();
+        dialog.destroy();
+    },
+
+    _onExportActivated: function() {
+        if (this._mapView.view.state === Champlain.State.DONE) {
+            this._activateExport();
+        } else {
+            let notifyId = this._mapView.view.connect('notify::state', (function() {
+                if (this._mapView.view.state === Champlain.State.DONE) {
+                    this._mapView.view.disconnect(notifyId);
+                    this._activateExport();
+                }
+            }).bind(this));
+        }
     }
 });
diff --git a/src/exportViewDialog.js b/src/exportViewDialog.js
new file mode 100644
index 0000000..2894ac9
--- /dev/null
+++ b/src/exportViewDialog.js
@@ -0,0 +1,174 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * GNOME Maps 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.
+ *
+ * GNOME Maps 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 GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jonas Danielson <jonas threetimestwo org>
+ */
+
+const Cairo = imports.cairo;
+const Gdk = imports.gi.Gdk;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const Utils = imports.utils;
+
+const Response = {
+    SUCCESS: 0,
+    CANCEL: 1
+};
+
+const _PREVIEW_WIDTH = 150;
+
+const ExportViewDialog = new Lang.Class({
+    Name: 'ExportViewDialog',
+    Extends: Gtk.Dialog,
+    Template: 'resource:///org/gnome/Maps/ui/export-view-dialog.ui',
+    InternalChildren: [ 'exportButton',
+                        'cancelButton',
+                        'filenameEntry',
+                        'fileChooserButton',
+                        'previewArea' ],
+
+    _init: function(params) {
+        this._surface = params.surface;
+        delete params.surface;
+
+        this._latitude = params.latitude;
+        delete params.latitude;
+
+        this._longitude = params.longitude;
+        delete params.longitude;
+
+        this._mapView = params.mapView;
+        delete params.mapView;
+
+        params.use_header_bar = true;
+        this.parent(params);
+
+        this._cancelButton.connect('clicked',
+                                   this.response.bind(this, Response.CANCEL));
+        this._exportButton.connect('clicked', this._exportView.bind(this));
+        this._filenameEntry.connect('changed',
+                                    this._onFileNameChanged.bind(this));
+        this._fileChooserButton.connect('file-set',
+                                        this._onFolderChanged.bind(this));
+
+
+        this._folder = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES);
+        if (!this._folder)
+            this._folder = GLib.get_user_data_dir();
+
+        this._filenameEntry.text = this._fileName = this._getName();
+        this._fileChooserButton.set_current_folder(this._folder);
+        this._setupPreviewArea();
+    },
+
+    _getName: function() {
+        return 'Maps at %f, %f.png'.format(this._latitude.toFixed(2),
+                                           this._longitude.toFixed(2));
+    },
+
+    _setupPreviewArea: function() {
+        let [surfaceWidth, surfaceHeight] = this._mapView.view.get_size();
+
+        let width = _PREVIEW_WIDTH;
+        this._scaleFactor = width / surfaceWidth;
+        let height = surfaceHeight * this._scaleFactor;
+
+        this._previewArea.set_size_request(width, height);
+        this._previewArea.connect('draw', this._drawPreview.bind(this));
+    },
+
+    _drawPreview: function(widget, cr) {
+        cr.setOperator(Cairo.Operator.CLEAR);
+        cr.paint();
+        cr.setOperator(Cairo.Operator.OVER);
+
+        cr.scale(this._scaleFactor, this._scaleFactor)
+        cr.setSourceSurface(this._surface, 0, 0);
+        cr.paint();
+    },
+
+    _onFileNameChanged: function() {
+        let name = GLib.filename_from_utf8(this._filenameEntry.text, -1)[0];
+        name = name.toString();
+        if (!name) {
+            this._exportButton.sensitive= false;
+            return;
+        }
+
+        try {
+            GLib.build_filenamev([this._folder, name]);
+            this._exportButton.sensitive = true;
+            this._fileName = name;
+        } catch(e) {
+            this._exportButton.sensitive = false;
+        }
+    },
+
+    _onFolderChanged: function() {
+        let folder = this._fileChooserButton.get_filename();
+
+        if (!GLib.file_test(folder, GLib.FileTest.IS_DIR)) {
+            this._exportButton.sensitive= false;
+            return;
+        }
+        if (!GLib.file_test(folder, GLib.FileTest.EXISTS)) {
+            this._exportButton.sensitive = false;
+            return;
+        }
+
+        this._exportButton.sensitive = true;
+        this._folder = folder;
+    },
+
+    _exportView: function() {
+        let [width, height] = this._mapView.view.get_size();
+        let pixbuf = Gdk.pixbuf_get_from_surface(this._surface, 0, 0, width, height);
+        let path = GLib.build_filenamev([this._folder, this._fileName]);
+
+        try {
+            pixbuf.savev(path, "png", [], []);
+            this.response(Response.SUCCESS);
+        } catch(e) {
+            Utils.debug('failed to export view: ' + e.message);
+            let details = null;
+
+            if (e.matches(GLib.FileError, GLib.FileError.ROFS))
+                details = _("Filesystem is read only");
+            else if (e.matches(GLib.FileError, GLib.FileError.ACCES))
+                details = _("You do not have permission to save there");
+            else if (e.matches(GLib.FileError, GLib.FileError.NOENT))
+                details = _("The directory does not exists");
+            else if (e.matches(GLib.FileError, GLib.FileError.ISDIR))
+                details = _("No filename specified");
+
+            let dialog = new Gtk.MessageDialog({
+                transient_for: this,
+                destroy_with_parent: true,
+                message_type: Gtk.MessageType.ERROR,
+                buttons: Gtk.ButtonsType.OK,
+                modal: true,
+                text: _("Unable to export view"),
+                secondary_text: details
+            });
+
+            dialog.run();
+            dialog.destroy();
+        }
+    }
+});
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index 6a10a06..8d171f1 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -9,6 +9,7 @@
     <file>contactPlace.js</file>
     <file>contextMenu.js</file>
     <file>epaf.js</file>
+    <file>exportViewDialog.js</file>
     <file>facebookBackend.js</file>
     <file>favoritesPopover.js</file>
     <file>foursquareBackend.js</file>


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