From fd81565c5527b99dd212b03c2f329a00e146f03b Mon Sep 17 00:00:00 2001 From: Rory MacQueen Date: Thu, 15 Aug 2013 15:41:36 -0700 Subject: Reorganized import paths Moved all widgets into widgets directory. Changed Endless Wikipedia file to expose wikipedia web view [endlessm/eos-sdk#260] --- wikipedia/widgets/BackButton.js | 18 +++ wikipedia/widgets/BoxWithBg.js | 19 ++++ wikipedia/widgets/SideBarButton.js | 39 +++++++ wikipedia/widgets/TextButton.js | 58 ++++++++++ wikipedia/widgets/category_button.js | 146 ++++++++++++++++++++++++ wikipedia/widgets/category_layout_manager.js | 65 +++++++++++ wikipedia/widgets/category_selector_view.js | 43 ++++++++ wikipedia/widgets/scaled_image.js | 159 +++++++++++++++++++++++++++ wikipedia/widgets/title_label_view.js | 95 ++++++++++++++++ 9 files changed, 642 insertions(+) create mode 100644 wikipedia/widgets/BackButton.js create mode 100644 wikipedia/widgets/BoxWithBg.js create mode 100644 wikipedia/widgets/SideBarButton.js create mode 100644 wikipedia/widgets/TextButton.js create mode 100644 wikipedia/widgets/category_button.js create mode 100644 wikipedia/widgets/category_layout_manager.js create mode 100644 wikipedia/widgets/category_selector_view.js create mode 100644 wikipedia/widgets/scaled_image.js create mode 100644 wikipedia/widgets/title_label_view.js (limited to 'wikipedia/widgets') diff --git a/wikipedia/widgets/BackButton.js b/wikipedia/widgets/BackButton.js new file mode 100644 index 0000000..cfb3155 --- /dev/null +++ b/wikipedia/widgets/BackButton.js @@ -0,0 +1,18 @@ +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const BackButton = new Lang.Class({ + Name: 'BackButton', + Extends: Gtk.Button, + + _init: function(props) { + props = props || {}; + props.image = Gtk.Image.new_from_icon_name('go-previous-symbolic', + Gtk.IconSize.BUTTON); + props.always_show_image = true; + // Don't do that. What should actually happen is the system-wide setting + // that controls whether buttons show images should be changed. + this.parent(props); + } +}); diff --git a/wikipedia/widgets/BoxWithBg.js b/wikipedia/widgets/BoxWithBg.js new file mode 100644 index 0000000..1ae9404 --- /dev/null +++ b/wikipedia/widgets/BoxWithBg.js @@ -0,0 +1,19 @@ +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const BoxWithBg = new Lang.Class({ + Name: "BoxWithBg", + Extends: Gtk.Box, + + vfunc_draw: function(cairoContext) { + let width = this.get_allocated_width(); + let height = this.get_allocated_height(); + let context = this.get_style_context(); + Gtk.render_background(context, cairoContext, 0, 0, width, height); + Gtk.render_frame(context, cairoContext, 0, 0, width, height); + + return this.parent(cairoContext); + } +}); + diff --git a/wikipedia/widgets/SideBarButton.js b/wikipedia/widgets/SideBarButton.js new file mode 100644 index 0000000..b602d6c --- /dev/null +++ b/wikipedia/widgets/SideBarButton.js @@ -0,0 +1,39 @@ +const Lang = imports.lang; +const Gdk = imports.gi.Gdk; +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gtk = imports.gi.Gtk; + +const SideBarButton = new Lang.Class({ + Name: 'EndlessSideBarButton', + Extends: Gtk.Button, + + // This is a button for the article list widget. It has a label and an icon image. + // The icon image will only appear on hover or press of button + _init: function(hover_icon_path, params) { + this.parent(params); + + this.set_size_request(40, -1); + + this._image = new Gtk.Image({ + resource: hover_icon_path, + no_show_all: true + }); + + this.add(this._image); + + this.connect('state-changed', Lang.bind(this, this._update_appearance)); + }, + + _update_appearance: function(widget, state) { + // If button is hovered over and/or pressed, then show the arrow icon + if (widget.get_state_flags() & Gtk.StateFlags.ACTIVE || + widget.get_state_flags() & Gtk.StateFlags.PRELIGHT) { + this._image.show(); + return false; // don't block event + } + // If no hover or press, then hide the arrow icon + this._image.hide(); + return false; // don't block event + } +}); + diff --git a/wikipedia/widgets/TextButton.js b/wikipedia/widgets/TextButton.js new file mode 100644 index 0000000..834826f --- /dev/null +++ b/wikipedia/widgets/TextButton.js @@ -0,0 +1,58 @@ +const Lang = imports.lang; +const Gdk = imports.gi.Gdk; +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gtk = imports.gi.Gtk; +const Pango = imports.gi.Pango; + +// This is an approximate number of characters that will keep the label from +// going over its specified width +const ARTICLE_LABEL_MAX_WIDTH_CHARS = 24; + +const TextButton = new Lang.Class({ + Name: 'EndlessTextButton', + Extends: Gtk.Button, + + // This is a button for the article list widget. It has a label and an icon image. + // The icon image will only appear on hover or press of button + _init: function(hover_icon_path, label_text, params) { + params.hexpand = true; + this.parent(params); + + this._hover_icon_pixbuf = GdkPixbuf.Pixbuf.new_from_resource(hover_icon_path); + + this._image = new Gtk.Image({ + no_show_all: true + }); + this._image.set_from_pixbuf(this._hover_icon_pixbuf); + + this._box = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL + }); + + this._label = new Gtk.Label({ + label: label_text.toUpperCase(), + max_width_chars: ARTICLE_LABEL_MAX_WIDTH_CHARS, + ellipsize: Pango.EllipsizeMode.END + }); + + this._box.pack_start(this._label, false, false, 0); + this._box.pack_end(this._image, false, false, 0); + + this.add(this._box); + this.connect('state-changed', Lang.bind(this, this._update_appearance)); + this.show_all(); + }, + + _update_appearance: function(widget, state) { + // If button is hovered over and/or pressed, then show the arrow icon + if (widget.get_state_flags() & Gtk.StateFlags.ACTIVE || + widget.get_state_flags() & Gtk.StateFlags.PRELIGHT) { + this._image.show(); + return false; + } + // If no hover or press, then hide the arrow icon + this._image.hide(); + return false; + } +}); + diff --git a/wikipedia/widgets/category_button.js b/wikipedia/widgets/category_button.js new file mode 100644 index 0000000..77812f1 --- /dev/null +++ b/wikipedia/widgets/category_button.js @@ -0,0 +1,146 @@ +const Gdk = imports.gi.Gdk; +const GdkPixbuf = imports.gi.GdkPixbuf; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const Utils = imports.wikipedia.utils; + +const CATEGORY_LABEL_LEFT_MARGIN = 25; // pixels +const CATEGORY_LABEL_BOTTOM_MARGIN = 20; // pixels +const CATEGORY_BUTTON_RIGHT_MARGIN = 20; // pixels +const CATEGORY_BUTTON_BOTTOM_MARGIN = 20; // pixels +const CATEGORY_LABEL_BENTON_SANS_CORRECTION = 0; // pixels +const _HOVER_ARROW_URI = '/com/endlessm/wikipedia-domain/assets/category_hover_arrow.png'; + +GObject.ParamFlags.READWRITE = GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE; + +const CategoryButton = new Lang.Class({ + Name: 'CategoryButton', + Extends: Gtk.EventBox, + Properties: { + // resource URI for the category's accompanying image + 'image-uri': GObject.ParamSpec.string('image-uri', + 'Image URI', + 'Resource URI for the image file accompanying the category', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + ''), + + // Title of the category to display + 'category-title': GObject.ParamSpec.string('category-title', + 'Category title', + 'Display name for the category', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + '') + }, + Signals: { + 'clicked': {} + }, + + _init: function(props) { + // Get ready for property construction + this._image_uri = null; + this._category_title = null; + + this._overlay = new Gtk.Overlay(); + this._eventbox = new Gtk.EventBox({ + expand: true + }); + this._eventbox_grid = new Gtk.Grid({ + orientation: Gtk.Orientation.HORIZONTAL, + hexpand: true, + valign: Gtk.Align.END + }); + this._label = new Gtk.Label({ + margin_left: CATEGORY_LABEL_LEFT_MARGIN, + margin_bottom: CATEGORY_LABEL_BOTTOM_MARGIN - CATEGORY_LABEL_BENTON_SANS_CORRECTION, + hexpand: true, + halign: Gtk.Align.START + }); + this._arrow = new Gtk.Image({ + resource: _HOVER_ARROW_URI, + margin_right: CATEGORY_BUTTON_RIGHT_MARGIN, + margin_bottom: CATEGORY_BUTTON_BOTTOM_MARGIN - CATEGORY_LABEL_BENTON_SANS_CORRECTION, + halign: Gtk.Align.END, + no_show_all: true + }); + + this._eventbox.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | + Gdk.EventMask.LEAVE_NOTIFY_MASK); + this._eventbox.connect('enter-notify-event', + Lang.bind(this, function(widget, event) { + this._eventbox.set_state_flags(Gtk.StateFlags.PRELIGHT, false); + this._arrow.show(); + })); + this._eventbox.connect('leave-notify-event', + Lang.bind(this, function(widget, event) { + this._eventbox.unset_state_flags(Gtk.StateFlags.PRELIGHT); + this._arrow.hide(); + })); + + let context = this._label.get_style_context(); + context.add_class(EndlessWikipedia.STYLE_CLASS_TITLE); + context.add_class(EndlessWikipedia.STYLE_CLASS_CATEGORY); + context.add_class(EndlessWikipedia.STYLE_CLASS_FRONT_PAGE); + this._image = new Gtk.Image({ + expand: true, + halign: Gtk.Align.FILL, + valign: Gtk.Align.FILL + }); + + // Parent constructor sets all properties + this.parent(props); + + // Put widgets together + this.add(this._overlay); + this._overlay.add(this._image); + this._eventbox_grid.add(this._label); + this._eventbox_grid.add(this._arrow); + this._eventbox.add(this._eventbox_grid); + this._overlay.add_overlay(this._eventbox); + this.show_all(); + + // Connect signals + this.connect('button-press-event', + Lang.bind(this, this._onButtonPress)); + }, + + get image_uri() { + return this._image_uri; + }, + + set image_uri(value) { + this._image_uri = value; + if(this._image) { + let allocation = this.get_allocation(); + let new_pixbuf = Utils.load_pixbuf_cover(Utils.resourceUriToPath(this._image_uri), + allocation.width, allocation.height); + this._image.set_from_pixbuf(new_pixbuf); + } + }, + + get category_title() { + return this._category_title; + }, + + set category_title(value) { + this._category_title = value; + if(this._label) + this._label.set_text(value.toUpperCase()); + }, + + // OVERRIDES + + vfunc_size_allocate: function(allocation) { + this.parent(allocation); + let new_pixbuf = Utils.load_pixbuf_cover(Utils.resourceUriToPath(this._image_uri), + allocation.width, allocation.height); + this._image.set_from_pixbuf(new_pixbuf); + }, + + // HANDLERS + + _onButtonPress: function(widget, event) { + this.emit('clicked') + } +}); diff --git a/wikipedia/widgets/category_layout_manager.js b/wikipedia/widgets/category_layout_manager.js new file mode 100644 index 0000000..a7be3bb --- /dev/null +++ b/wikipedia/widgets/category_layout_manager.js @@ -0,0 +1,65 @@ +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const CategoryLayoutManager = new Lang.Class({ + Name: 'CategoryLayoutManager', + Extends: Gtk.Grid, + + _init: function(props) { + props = props || {}; + props.column_homogeneous = true; + props.row_homogeneous = true; + this.parent(props); + + this._childWidgets = []; + }, + + // Distribute children in two columns, except for the last one if an odd + // number; that should span two columns + _redistributeChildren: function() { + let numChildren = this._childWidgets.length; + let oddNumber = numChildren % 2 == 1; + this._childWidgets.forEach(function(child, index) { + let column = index % 2; + let row = Math.floor(index / 2); + + if(child.get_parent() === this) + Gtk.Container.prototype.remove.call(this, + this._childWidgets[index]); + + if(oddNumber && index == numChildren - 1) + this.attach(child, 0, row, 2, 1); + else + this.attach(child, column, row, 1, 1); + }, this); + }, + + add: function(child) { + this._childWidgets.push(child); + this._redistributeChildren(); + }, + + remove: function(child) { + let index = this._childWidgets.indexOf(child); + if(index == -1) { + printerr('Widget', System.addressOf(child), + 'is not contained in CategoryLayoutManager'); + return; + } + this._childWidgets.splice(index, 1); // remove + this._redistributeChildren(); + } +}); + +// Gtk.init(null); +// let w = new Gtk.Window(); +// let g = new CategoryLayoutManager(); +// let count = 7; +// for(let i = 0; i < count; i++) { +// let widget = new Gtk.Button({label: 'Widget ' + i}); +// g.add(widget); +// } +// w.add(g); +// w.connect('destroy', Gtk.main_quit); +// w.show_all(); +// Gtk.main(); diff --git a/wikipedia/widgets/category_selector_view.js b/wikipedia/widgets/category_selector_view.js new file mode 100644 index 0000000..776bf52 --- /dev/null +++ b/wikipedia/widgets/category_selector_view.js @@ -0,0 +1,43 @@ +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const CategoryButton = imports.wikipedia.widgets.category_button; +const CategoryLayoutManager = imports.wikipedia.widgets.category_layout_manager; + +const CATEGORY_COLUMN_SPACING = 10; // pixels +const CATEGORY_ROW_SPACING = 10; // pixels + +const CategorySelectorView = new Lang.Class({ + Name: 'CategorySelectorView', + Extends: CategoryLayoutManager.CategoryLayoutManager, + Signals: { + 'category-chosen': { + param_types: [GObject.TYPE_STRING, GObject.TYPE_INT] + } + }, + + _init: function(props) { + props = props || {}; + props.column_spacing = CATEGORY_COLUMN_SPACING; + props.row_spacing = CATEGORY_ROW_SPACING; + this.parent(props); + }, + + // Takes an array of dictionaries with keys 'title' and 'image_uri' + setCategories: function(categories) { + categories.forEach(function(category, index, obj) { + let button = new CategoryButton.CategoryButton({ + category_title: category.title, + image_uri: category.image_thumbnail_uri + }); + button.index = index; + button.connect('clicked', Lang.bind(this, this._onButtonClicked)); + this.add(button); + }, this); + }, + + _onButtonClicked: function(button) { + this.emit('category-chosen', button.category_title, button.index); + } +}); \ No newline at end of file diff --git a/wikipedia/widgets/scaled_image.js b/wikipedia/widgets/scaled_image.js new file mode 100644 index 0000000..9808914 --- /dev/null +++ b/wikipedia/widgets/scaled_image.js @@ -0,0 +1,159 @@ +const Format = imports.format; +const GdkPixbuf = imports.gi.GdkPixbuf; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +String.prototype.format = Format.format; +GObject.ParamFlags.READWRITE = GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE; + +const BACKGROUND_CSS_TEMPLATE = "\ +Gjs_ScaledImage {\n\ + background-image: url('resource://%s');\n\ + background-size: %s;\n\ + background-position: %s %s;\n\ + background-repeat: no-repeat;\n\ +}"; + +const ScaledImage = new Lang.Class({ + Name: 'ScaledImage', + Extends: Gtk.EventBox, + Properties: { + 'constraint': GObject.ParamSpec.enum('constraint', + 'Constraint direction', + 'Orientation in which the size of the image should be constrained', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + Gtk.Orientation, Gtk.Orientation.HORIZONTAL), + 'resource': GObject.ParamSpec.string('resource', + 'Resource path', + 'Resource path for the image', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + '') + }, + + _init: function(props) { + this._constraint = null; + this._resource_path = null; + this._pixbuf = null; + this._css_provider = null; + this.parent(props); + }, + + // OVERRIDES + + vfunc_get_request_mode: function() { + if(this._constraint == Gtk.Orientation.HORIZONTAL) + return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; + return Gtk.SizeRequestMode.WIDTH_FOR_HEIGHT; + }, + + vfunc_get_preferred_width_for_height: function(height) { + if(!this._pixbuf) + return this.parent(height); + let source_width = this._pixbuf.width; + let source_height = this._pixbuf.height; + let width = (height / source_height) * source_width; + return [width, width]; + }, + + vfunc_get_preferred_height_for_width: function(width) { + if(!this._pixbuf) + return this.parent(width); + let source_width = this._pixbuf.width; + let source_height = this._pixbuf.height; + let height = (width / source_width) * source_height; + return [height, height]; + }, + + vfunc_size_allocate: function(allocation) { + if(this._constraint == Gtk.Orientation.VERTICAL + && this.valign != Gtk.Align.FILL) { + printerr("ScaledImage Warning: Setting constraint to VERTICAL and\ + valign to anything but FILL makes no sense"); + this.valign = Gtk.Align.FILL; + } + if(this._constraint == Gtk.Orientation.HORIZONTAL + && this.halign != Gtk.Align.FILL) { + printerr("ScaledImage Warning: Setting constraint to HORIZONTAL and\ + halign to anything but FILL makes no sense"); + this.halign = Gtk.Align.FILL; + } + this.parent(allocation); + }, + + // PROPERTIES + + get constraint() { + return this._constraint; + }, + + set constraint(value) { + this._constraint = value; + }, + + get resource() { + return this._resource_path; + }, + + set resource(value) { + this._resource_path = value; + this._pixbuf = GdkPixbuf.Pixbuf.new_from_resource(this._resource_path); + this._updateImage(); + }, + + // PRIVATE + + _gtk_align_to_css_align: function(align, orientation) { + switch(align) { + case Gtk.Align.START: + if(orientation == Gtk.Orientation.VERTICAL) + return "top"; + return "left"; + case Gtk.Align.END: + if(orientation == Gtk.Orientation.VERTICAL) + return "bottom"; + return "right"; + } + return "center"; + }, + + _updateImage: function() { + if(this._resource_path === null) + return; + + let context = this.get_style_context(); + + if(this._css_provider !== null) + context.remove_provider(this._css_provider); + + let scaling; + if(this._constraint == Gtk.Orientation.HORIZONTAL) + scaling = "100% auto"; + else + scaling = "auto 100%"; + + let css = BACKGROUND_CSS_TEMPLATE.format(this._resource_path, scaling, + this._gtk_align_to_css_align(this.valign, Gtk.Orientation.VERTICAL), + this._gtk_align_to_css_align(this.halign, + Gtk.Orientation.HORIZONTAL)); + this._css_provider = new Gtk.CssProvider(); + this._css_provider.load_from_data(css); + context.add_provider(this._css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_USER); + } +}); + +// const Gio = imports.gi.Gio; +// Gtk.init(null); +// let resource = Gio.Resource.load('data/endless_brazil.gresource'); +// resource._register(); +// let w = new Gtk.Window(); +// let i = new ScaledImage({ +// resource: '/com/endlessm/brazil/category_images/cuisine.jpg', +// constraint: Gtk.Orientation.HORIZONTAL, +// valign: Gtk.Align.END, +// }); +// w.add(i); +// w.connect('destroy', Gtk.main_quit); +// w.show_all(); +// Gtk.main(); diff --git a/wikipedia/widgets/title_label_view.js b/wikipedia/widgets/title_label_view.js new file mode 100644 index 0000000..f3f599e --- /dev/null +++ b/wikipedia/widgets/title_label_view.js @@ -0,0 +1,95 @@ +const GdkPixbuf = imports.gi.GdkPixbuf; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const Utils = imports.wikipedia.utils; + +const TITLE_LABEL_SCREEN_WIDTH_PERCENTAGE = 0.37; +const TITLE_LABEL_LEFT_MARGIN = 20; // pixels +const TITLE_LABEL_BOTTOM_MARGIN = 20; // pixels +const TITLE_LABEL_BENTON_SANS_CORRECTION = 20; // pixels + +const TitleLabelView = new Lang.Class({ + Name: 'TitleLabelView', + Extends: Gtk.Overlay, + Properties: { + 'title': GObject.ParamSpec.string('title', + 'Front page title', + 'Name of the Wikipedia-based application', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + ''), + 'image-uri': GObject.ParamSpec.string('image-uri', + 'Image URI', + 'Image URI for title image', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + '') + }, + + _init: function(props) { + this._title = null; + this._image_uri = null; + this._label = new Gtk.Label({ + halign: Gtk.Align.START, + valign: Gtk.Align.END, + margin_left: TITLE_LABEL_LEFT_MARGIN, + margin_bottom: TITLE_LABEL_BOTTOM_MARGIN - TITLE_LABEL_BENTON_SANS_CORRECTION + }); + this._image = new Gtk.Image(); + + let context = this._label.get_style_context() + context.add_class(EndlessWikipedia.STYLE_CLASS_TITLE); + + this.parent(props); + + this.add(this._image); + this.add_overlay(this._label); + }, + + // OVERRIDES + + // Ensure that this widget is 37% of the window's width + vfunc_get_preferred_width: function() { + let toplevel = this.get_toplevel(); + if(toplevel == null) + return this.parent(); + let width = toplevel.get_allocated_width() * TITLE_LABEL_SCREEN_WIDTH_PERCENTAGE; + return [width, width]; + }, + + vfunc_size_allocate: function(allocation) { + this.parent(allocation); + if(this._image_uri !== "" && this._image_uri != null) { + let new_pixbuf = Utils.load_pixbuf_cover(Utils.resourceUriToPath(this._image_uri), + allocation.width, allocation.height); + this._image.set_from_pixbuf(new_pixbuf); + } + }, + + // PROPERTIES + + get title() { + return this._title; + }, + + set title(value) { + this._title = value; + if(this._label) + this._label.label = value.toUpperCase(); + }, + + get image_uri() { + return this._image_uri; + }, + + set image_uri(value) { + this._image_uri = value; + if(this._image) { + let res_path = Utils.resourceUriToPath(value); + let allocation = this.get_allocation(); + let new_pixbuf = Utils.load_pixbuf_cover(Utils.resourceUriToPath(this._image_uri), + allocation.width, allocation.height); + this._image.set_from_pixbuf(new_pixbuf); + } + } +}); -- cgit v1.2.3