absolute fixed positioning in GTK.Fixed - gtk
For learning, I've written a toolframe class (gtk 3.24.20), the toolframes are moveable and expandable.
All toolframes are appended to a Gtk.Fixed widget.
documentation about Gtk.Fixed "The GtkFixed widget is a container which can place child widgets at fixed positions and with fixed sizes, given in pixels. GtkFixed performs no automatic layout management"
If i use the instance methods of Gtk.Fixed put and move to put the toolframe to (100,100) , sometimes the gtk layout calculation put the toolframe back to the orginal position (0,0).
for example if i'm resizing the window. or click on a button
if i use widget method size_allocate and set x,y, width and height, it also happens, suddenly the width changes and x,y goes back to 0,0
i had to watch the signal size-allocate and every time, if gtk layout changes my toolframe's x,y, width height dimensions, i have to overwrite the allocation to put it back to my position, width and height.
I wonder, because documentation says. on Gtk.Fixed no layout managment is performed.
Do i really have to always overwrite the allocation, when the size-allocate signal is fired for my toolframe, or did i something wrong.
Another question- if i want to destroy a GOBJECT, do i have to disconnect all signal handlers before destroying, or can i simply destroy it, without disconnecting (about memory leaks)
class with an example:
#!/usr/bin/gjs
const Gtk= imports.gi.Gtk,
Gio= imports.gi.Gio,
Gdk= imports.gi.Gdk,
GdkPixbuf= imports.gi.GdkPixbuf;
Gtk.init(null);
const window= new Gtk.Window(),
fixed_container= new Gtk.Fixed();
/* ==================================================================================================================================
CLASS ToolFrame (fixed_container, x, y, image_URL_orPATH_orICON, title, tooltip_text, content)
==================================================================================================================================
gtk version: 3.24.20 - depends on Gtk, Gio, Gdk, GdkPixbuf
--------------------
creates a moveable toolframe with titlebar and content, the toolframe can be minimized only showing the titlebar or can be expanded
most of the toolframe style can be changed over static object ToolFrame.standards (for explaination look at ToolFrame.standards)
before creation. In standard the toolframe has four buttons, a minimize button, an expand button, a pin button, and a close button
Gtk widget structure:
---------------------
eventbox_for_vbox (root widget) -> vbox -> eventbox_for_hbox -> hbox (titlebar)-> image, label, buttons
-> scrolled window -> content
parameters:
-----------
fixed_container GOBJ ... a Gtk.Fixed widget - parent container for all ToolFrame instances
x NUMB (INT) ... x position of the ToolFrame instance relative to parent container origin
y NUMB (INT) ... y position of the ToolFrame instance relative to parent container origin
image_URL_orPATH_orICON STR ... can be an URL or a PATH or an ICON name, icon names have to start with "icon://",
null or undefined, means no image
title STR ... the title of the ToolFrame instance
tool_tip STR ... tooltip text (hovering title) - null or undefined means no tooltip
content GOBJ ... a Gtk widget which is shown, when the ToolFrame instance is expanded
for detailed explainations look inside Gnome JavaScript class comments!
public instance properties:
---------------------------
fixed_container, root_widget, widgets, style_contexts, isPinned, isExpanded
public prototype properties:
----------------------------
toolframes, toolframe_inAction
public instance methods:
------------------------
set_AsLastChild, move, get_expanded_size, set_expanded_size
-----------------------------------------------------------------------------------------------------------------------------------*/
{ //-----> blockscope Class ToolFrame
//-----------------------------------------------------------------------
//----------------- CONSTRUCTOR
var ToolFrame=function ToolFrame (fixed_container, x, y, image_URL_orPATH_orICON, title, tooltip_text, content)
{
const widgets=new Object (), standards=ToolFrame.standards, style_contexts=new Object ();
var width, height, cssProvider;
widgets.eventbox_for_hbox= new Gtk.EventBox(); //-- eventbox parent of hbox
widgets.eventbox_for_vbox= new Gtk.EventBox(); //-- eventbox parent of vbox -> ToolFrame instance root_widget!
widgets.hbox= new Gtk.Box({ spacing: standards.titlebar_spacing, valign:3 }); //-- titlebar = hbox
widgets.vbox= new Gtk.Box({ orientation:1, spacing: standards.toolframe_spacing}); //-- children hbox (titlebar) and scrolled_window (holds content)
widgets.scrolled_window= new Gtk.ScrolledWindow ({ vexpand:true }); //-- content added to scrolled_window
//-- references to toolframe instance used by signals functions (circular references)
widgets.eventbox_for_vbox.toolframe=this;
widgets.hbox.toolframe=this;
//-- creating titlebar image and pixbuf, width and height defined in ToolFrame.standards
if (image_URL_orPATH_orICON)
{
widgets.image= new Gtk.Image({ halign:3, valign:3 }); //-- vertical center
//-- ICON case
if (image_URL_orPATH_orICON.indexOf("icon://")==0) widgets.image.set_from_icon_name (image_URL_orPATH_orICON.slice (7,image_URL_orPATH_orICON.length), standards.icon_size);
//-- URL or PATH
else
{
widgets.imagePixBuf=get_pixbuf_from_URLorPATH (image_URL_orPATH_orICON);
if (widgets.imagePixBuf)
{
if (standards.image_width.constructor === Number)
{
width=standards.image_width;
if (standards.image_height =="auto") height=Math.round (width/widgets.imagePixBuf.width*widgets.imagePixBuf.height);
}
if (standards.image_height.constructor === Number)
{
height=standards.image_height;
if (standards.image_width == "auto") width=Math.round (height/widgets.imagePixBuf.height*widgets.imagePixBuf.width);
}
widgets.image.set_from_pixbuf (widgets.imagePixBuf.scale_simple (width, height, GdkPixbuf.InterpType.BILINEAR));
}
}
widgets.hbox.pack_start(widgets.image,false,false,0);
}
//-- titlebar title
widgets.label= new Gtk.Label ({ label:title }); widgets.hbox.pack_start(widgets.label,false,false,0);
cssProvider=new Gtk.CssProvider();
cssProvider.load_from_data ("* "+standards.title_css_STR);
widgets.label.get_style_context().add_provider (cssProvider,0);
if (tooltip_text) widgets.label.set_tooltip_text (tooltip_text);
//-- determine titlebar buttons width and height, defined in ToolFrame.standards
if (standards.button_width.constructor === Number) width=standards.button_width; else width=-1;
if (standards.button_height.constructor === Number) height=standards.button_height; else height=-1;
//-- creating titlebar buttons according to ToolFrame.standards
cssProvider=new Gtk.CssProvider();
cssProvider.load_from_data ("* "+standards.buttons_css_STR);
if (standards.buttons_creation.close)
{
widgets.button_close= new Gtk.Button ({ label:standards.button_close_char, halign:3, valign:3 }),
widgets.button_close.connect("clicked", button_close_clicked);
widgets.hbox.pack_end(widgets.button_close,false,false,0);
widgets.button_close.set_size_request (width, height);
widgets.button_close.get_style_context().add_provider(cssProvider,0);
if (standards.button_tooltip) widgets.button_close.set_tooltip_text("close");
}
if (standards.buttons_creation.pin)
{
if (standards.isPinned) widgets.button_pin=new Gtk.Button ({ label:standards.button_pinned_char, halign:3, valign:3 });
else widgets.button_pin=new Gtk.Button ({label:standards.button_pin_char, halign:3, valign:3 });
widgets.button_pin.connect("clicked", button_pin_clicked);
widgets.hbox.pack_end(widgets.button_pin,false,false,0);
widgets.button_pin.set_size_request (width, height);
widgets.button_pin.get_style_context().add_provider(cssProvider,0);
if (standards.button_tooltip) widgets.button_pin.set_tooltip_text("pin");
}
if (standards.buttons_creation.minimize_expand)
{
widgets.button_expand= new Gtk.Button ({ label:standards.button_expand_char, halign:3, valign:3 }),
widgets.button_expand.connect("clicked", button_expand_clicked);
widgets.hbox.pack_end(widgets.button_expand,false,false,0);
widgets.button_expand.set_size_request (width, height);
widgets.button_expand.get_style_context().add_provider(cssProvider,0);
if (standards.button_tooltip) widgets.button_expand.set_tooltip_text("expand");
widgets.button_minimize= new Gtk.Button ({label:standards.button_minimize_char, halign:3, valign:3 }),
widgets.button_minimize.connect("clicked", button_minimize_clicked);
widgets.hbox.pack_end(widgets.button_minimize,false,false,0);
widgets.button_minimize.set_size_request (width, height);
widgets.button_minimize.get_style_context().add_provider(cssProvider,0);
if (standards.button_tooltip) widgets.button_minimize.set_tooltip_text("minimize");
//-- sensitivity button_minimize, button_expand
if (standards.content_visible_atStart) widgets.button_expand.set_sensitive(false);
else widgets.button_minimize.set_sensitive(false);
}
/* setting references for ToolFrame instance; fixed_container -> stage container for all ToolFrame instances
root_widget -> representing ToolFrame instance on screen; widgets -> folder for all widgets including image pixbuffer;
style_contexts -> holds the style context of vbox and hbox and some button chars */
widgets.content=content;
this.fixed_container=fixed_container; //-- the parent of all ToolFrame instances, a Gtk.Fixed
this.root_widget=widgets.eventbox_for_vbox; //-- the ToolFrame instance root widget, an eventbox (for vbox)
this.widgets=widgets; //-- an object, which holds references to all widgets inclusive imagePixBuf
this.style_contexts=style_contexts; //-- an object, which holds references to style contexts (hbox, vbox)
//-- adding buttons_pin.. characters to style_contexts
style_contexts.button_pin_char=standards.button_pin_char;
style_contexts.button_pinned_char=standards.button_pinned_char;
//-- css, style of hbox
cssProvider=new Gtk.CssProvider();
cssProvider.load_from_data(".divider "+standards.divider_css_borderSTR);
style_contexts.hbox=widgets.hbox.get_style_context();
if (standards.content_visible_atStart) style_contexts.hbox.add_class("divider");
style_contexts.hbox.add_provider(cssProvider,0);
if (standards.titlebar_spacing.constructor === Number) widgets.hbox.spacing=standards.titlebar_spacing;
if (standards.titlebar_margin_start.constructor === Number) widgets.hbox.margin_start=standards.titlebar_margin_start; //--set margin values
if (standards.titlebar_margin_end.constructor === Number) widgets.hbox.margin_end=standards.titlebar_margin_end;
if (standards.titlebar_margin_top.constructor === Number) widgets.hbox.top_right=standards.titlebar_margin_top;
if (standards.titlebar_margin_bottom.constructor === Number) widgets.hbox.bottom_right=standards.titlebar_margin_bottom;
//-- css, style of vbox
cssProvider=new Gtk.CssProvider();
cssProvider.load_from_data("* "+standards.toolframe_css_STR+" .moving "+standards.toolframe_move_css_STR);
style_contexts.vbox=widgets.vbox.get_style_context ();
style_contexts.vbox.add_provider (cssProvider,0);
widgets.vbox.override_background_color (Gtk.StateFlags.NORMAL, standards.toolframe_backgroundColor);
if (standards.toolframe_spacing.constructor === Number) widgets.vbox.spacing=standards.toolframe_spacing;
//-- set content margin values
if (standards.content_margin_start.constructor === Number) widgets.scrolled_window.margin_start=standards.content_margin_start; //--set margin values
if (standards.content_margin_end.constructor === Number) widgets.scrolled_window.margin_end=standards.content_margin_end;
if (standards.content_margin_top.constructor === Number) widgets.scrolled_window.top_right=standards.content_margin_top;
if (standards.content_margin_bottom.constructor === Number) widgets.scrolled_window.bottom_right=standards.content_margin_bottom;
//-- PRIVATE folder on instance - holds x,y (actual position), expanded_width and expanded_height
this[private_key]={
_x:x, _y:y,
_expanded_width:standards.toolframe_expanded_width,
_expanded_height:standards.toolframe_expanded_height}
//-- set toolframe minimum width and height
if (standards.toolframe_minimum_width.constructor === Number) width=standards.toolframe_minimum_width; else width=-1;
if (standards.toolframe_minimum_height.constructor === Number) height=standards.toolframe_minimum_height; else height=-1;
this.root_widget.set_size_request (width, height);
//-- build widget tree
widgets.eventbox_for_hbox.add (widgets.hbox);
widgets.vbox.add (widgets.eventbox_for_hbox);
widgets.scrolled_window.add (content);
widgets.vbox.add (widgets.scrolled_window);
widgets.eventbox_for_vbox.add (widgets.vbox);
fixed_container.add (widgets.eventbox_for_vbox);
//-- instance properties isPinned, isExpanded, push to toolframes prototype array collection
this.isPinned=standards.isPinned; //-- indicates, that the toolframe is pinned
if (standards.content_visible_atStart) this.isExpanded=true; //-- indicates, that the toolframe is expanded
else this.isExpanded=false;
ToolFrame.prototype.toolframes.push(this);
//-- connect to hbox, vbox signals
widgets.eventbox_for_hbox.connect("button-press-event",start_moving);
widgets.eventbox_for_hbox.connect("button-release-event",stop_moving);
widgets.eventbox_for_vbox.connect ("button-press-event",this.set_AsLastChild.bind(this));
widgets.eventbox_for_vbox.connect ("size-allocate",observe_root_widget.bind(this));
//-- in this handler the scrolled_window visibility is set to standards.content_visible_atStart at first show signal
this[private_key].handler_first_show_signal=widgets.scrolled_window.connect("show", observe_first_show_signal);
}
//----------------- CONSTRUCTOR -- END
//-----------------------------------------------------------------------
//------------------PROTOTYPE
ToolFrame.prototype.toolframes=[]; //-- a collection of all toolframes created
ToolFrame.prototype.toolframe_inAction=null; //-- the toolframe, which is moved, when moving takes place
//-- put the ToolFrame instance as last child of the fixed_container, its like increasing zIndex, to bring it on top
ToolFrame.prototype.set_AsLastChild=function ()
{
const children=this.fixed_container.get_children(), l=children.length;
if (this.root_widget !== children[l-1])
{
this.fixed_container.remove (this.root_widget);
this.fixed_container.add (this.root_widget);
}
}
//-- moves the toolframe to x,y (INT) position
ToolFrame.prototype.move=function (x,y)
{
const allocation=this.root_widget.get_allocation (), PRIVATE=this[private_key];
PRIVATE._x=allocation.x=x; PRIVATE._y=allocation.y=y;
observing_size_allocation=false;
this.root_widget.size_allocate (allocation);
observing_size_allocation=true;
}
/* sets the expanded_width and expanded_height, which are stored in a PRIVATE instance folder - width, height INT
you can set minimum_width and minimum_height over set_size_request on root_widget */
ToolFrame.prototype.set_expanded_size=function (width,height)
{
const PRIVATE=this[private_key];
PRIVATE._expanded_width=width; PRIVATE._expanded_height=height;
if (this.isExpanded) observe_root_widget.call (this, this.widgets.eventbox_for_vbox);
}
//-- gets the expanded_width and expanded_height stored in the PRIVATE instance folder
ToolFrame.prototype.get_expanded_size=function ()
{
const PRIVATE=this[private_key];
return { expanded_width: PRIVATE._expanded_width, expanded_height: PRIVATE._expanded_height };
}
//------------------PROTOTYPE -- END
//-----------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------
//----------------- PRIVATE objects, properties, methods
let private_key=Symbol (), //-- PRIVATE instance folder key
observing_size_allocation=true, //-- indicates, that the "size-allocate" signal shall be observed for uncontrollable automatic gtk allocation changes on the root widget
isMoving=false, //-- indicates, when moving shall happen inside window "motion-notify-event" signal
move_allocation=null, //-- holds the actual allocation during movement
moveStart_allocation=null, //-- saves the ToolFrame instance allocation (x,y,width,height) at move start
moveStart_eventCoords=null, //-- saves the pointer coordinates at move start -> difference in pointer coordinates equals difference in instance allocation (x,y)
titlebar_margin=0, content_margin=0, //-- saves standards titlebar_margin and content_margin
//-----------------------------------------------------------------------
//------------------------------------------------------ Signal functions
button_minimize_clicked=function (button)
{
const actual_toolframe=button.get_parent().toolframe, PRIVATE=actual_toolframe[private_key],
widgets=actual_toolframe.widgets;
widgets.scrolled_window.set_visible (false);
widgets.button_minimize.set_sensitive (false);
widgets.button_expand.set_sensitive (true);
actual_toolframe.style_contexts.hbox.remove_class ("divider");
actual_toolframe.isExpanded=false;
actual_toolframe.set_AsLastChild ();
},
button_expand_clicked=function (button)
{
const actual_toolframe=button.get_parent().toolframe, PRIVATE=actual_toolframe[private_key],
widgets=actual_toolframe.widgets;
widgets.scrolled_window.set_visible (true);
widgets.button_minimize.set_sensitive (true);
widgets.button_expand.set_sensitive (false);
actual_toolframe.style_contexts.hbox.add_class ("divider");
actual_toolframe.isExpanded=true;
actual_toolframe.set_AsLastChild ();
},
button_pin_clicked=function (button)
{
const actual_toolframe=button.get_parent().toolframe;
actual_toolframe.isPinned=!actual_toolframe.isPinned;
if (actual_toolframe.isPinned) actual_toolframe.widgets.button_pin.label=actual_toolframe.style_contexts.button_pinned_char;
else actual_toolframe.widgets.button_pin.label=actual_toolframe.style_contexts.button_pin_char;
actual_toolframe.set_AsLastChild ();
},
button_close_clicked=function (button) { button.get_parent().toolframe.root_widget.set_visible (false); },
//-- connected to eventbox_for_hbox signal "button-press-event" , initiate moving
start_moving=function (eventbox_for_hbox, event)
{
const actual_toolframe=eventbox_for_hbox.get_children()[0].toolframe;
var eventCoords;
if (actual_toolframe.isPinned) return;
eventCoords=event.get_root_coords(); //-- coords relative to eventbox_for_hbox origin (hbox)
isMoving=true;
move_allocation=actual_toolframe.widgets.eventbox_for_vbox.get_allocation ();
moveStart_allocation=actual_toolframe.widgets.eventbox_for_vbox.get_allocation ();
moveStart_eventCoords={
x: Math.trunc(eventCoords[1]),
y: Math.trunc(eventCoords[2])};
actual_toolframe.style_contexts.vbox.add_class("moving");
ToolFrame.prototype.toolframe_inAction=actual_toolframe;
},
//-- connected to eventbox_for_hbox signal "button-release-event", stops moving, set necessary values back to null
stop_moving=function (eventbox_for_hbox)
{
const actual_toolframe=eventbox_for_hbox.get_children()[0].toolframe;
isMoving=false;
move_allocation=null;
moveStart_allocation=null;
moveStart_eventCoords=null;
ToolFrame.prototype.toolframe_inAction=null;
actual_toolframe.style_contexts.vbox.remove_class("moving");
},
//-- connected to window signal "motion-notify-event", moves the toolframe with pointer device
move_toolframe=function (window, event)
{
var actual_toolframe, PRIVATE, eventCoords;
if (isMoving)
{
actual_toolframe=ToolFrame.prototype.toolframe_inAction, PRIVATE=actual_toolframe[private_key];
eventCoords=event.get_root_coords();
PRIVATE._x=move_allocation.x=Math.trunc(eventCoords[1])-moveStart_eventCoords.x+moveStart_allocation.x;
PRIVATE._y=move_allocation.y=Math.trunc(eventCoords[2])-moveStart_eventCoords.y+moveStart_allocation.y;
observing_size_allocation=false;
actual_toolframe.root_widget.size_allocate (move_allocation);
observing_size_allocation=true;
}
},
/* connected to eventbox_for_vbox signal "size-allocate", overwrites the gtk layout coords, dimensions
gtk layout replaces the tool frame and its dimensions according to its inner rules,
thats not very helpful, if the tool frame should be placed at different position, or should show up with different width and height, than gtk layout calculates.
so to force gtk to accept different dimensions, the gtk signal "size allocate" for the tool frame (eventbox_for_vbox) is observed */
observe_root_widget=function (eventbox_for_vbox)
{
var gtk_allocation, replacing, PRIVATE;
if (observing_size_allocation)
{
gtk_allocation=eventbox_for_vbox.get_allocation (), replacing=false, PRIVATE=eventbox_for_vbox.toolframe[private_key];
if (gtk_allocation.x != PRIVATE._x)
{
replacing=true;
gtk_allocation.x=PRIVATE._x;
}
if (gtk_allocation.y != PRIVATE._y)
{
replacing=true;
gtk_allocation.y=PRIVATE._y;
}
if (eventbox_for_vbox.toolframe.isExpanded)
{
if (PRIVATE._expanded_width.constructor === Number && gtk_allocation.width!=PRIVATE._expanded_width)
{
replacing=true;
gtk_allocation.width=PRIVATE._expanded_width;
}
if (PRIVATE._expanded_height.constructor === Number && gtk_allocation.height!=PRIVATE._expanded_height)
{
replacing=true;
gtk_allocation.height=PRIVATE._expanded_height;
}
}
if (replacing)
{
observing_size_allocation=false; //-- prohibit calls of size_allocate to infinity, because next line is causing another size-allocate event, which is immediately worked through
this.root_widget.size_allocate (gtk_allocation); //-- causes again a size-allocate event
this.root_widget.set_clip (gtk_allocation);
observing_size_allocation=true; //-- additionally size-allocate event is now worked through, allow observing again
}
}
},
/* connected to scrolled_window (holds content) signal "show"; is needed to set the visibility according to standards content_visible_atStart property at first show signal
after executing one time the handler is disconnected */
observe_first_show_signal=function (scrolled_window)
{
var PRIVATE=scrolled_window.get_parent().get_parent().toolframe[private_key];
scrolled_window.set_visible(ToolFrame.standards.content_visible_atStart);
scrolled_window.disconnect (PRIVATE.handler_first_show_signal);
PRIVATE.handler_first_show_signal=null
},
//------------------------------------------------------ Signal functions -- END
//-----------------------------------------------------------------------
//-- returns the image pixelbuffer from URL or PATH
get_pixbuf_from_URLorPATH=function (URLorPATH)
{
var file, pixbuf=null;
//-- URL contains //
if (URLorPATH.indexOf("//")>-1)
{
file=Gio.File.new_for_uri (URLorPATH);
try { pixbuf=GdkPixbuf.Pixbuf.new_from_stream (file.read (null), null); }
catch (error) { log (error+" - "+URLorPATH) }
return pixbuf;
}
else return GdkPixbuf.new_from_file (URLorPATH);
};
//-- connect to window signals
window.connect ("motion-notify-event", move_toolframe);
//----------------- PRIVATE objects, properties, methods
//------------------------------------------------------------------------------------------------------------------------------------
//----------------- STATIC object ToolFrame standards
ToolFrame.standards={
isPinned: false, //-- indicates, if the tool frame can move
toolframe_minimum_width: "auto", //-- minimum width of tool frame in minimize appearance
toolframe_minimum_height: "auto", //-- minimum height of tool frame in minimize apperance
toolframe_expanded_width: "auto", //-- width of tool frame in expand appearance
toolframe_expanded_height: "auto", //-- height of tool frame in expand appearance
image_width: "auto", //-- image minimum width and height
image_height: 24,
icon_size: Gtk.IconSize.LARGE_titlebar, //-- Look at Gtk.IconSize (24px)
button_width: "auto", //-- button minimum width and height
button_height: "auto",
toolframe_spacing: 2, titlebar_spacing: 2, //-- vbox, hbox spacing
titlebar_margin_start: 2, titlebar_margin_end:2, titlebar_margin_top:2, titlebar_margin_bottom:2, //-- hbox margin values
content_margin_start: 2, content_margin_end:2, content_margin_top:2, content_margin_bottom:2, //-- content margin values
content_visible_atStart: false, //-- indicates, if content shall be visible at start
buttons_creation: { minimize_expand:true, pin:true, close:true }, //-- indicates, which buttons shall be created
button_minimize_char: "\u25B4", //-- button characters
button_expand_char: "\u25BE",
button_pin_char: "\u21C4",
button_pinned_char: "\u21B9",
button_close_char: "X",
button_tooltip: true, //-- indicates, if tooltips shall be shown by hovering buttons
//-- css style strings
toolframe_css_STR: "{ border:1px solid black }",
toolframe_move_css_STR: "{ border-style:dashed }",
toolframe_backgroundColor: new Gdk.RGBA ({ red:1, green:1, blue:1, alpha:1}), //-- toolframe background color
title_css_STR: "{ font-weight:bold }",
buttons_css_STR: "{}",
divider_css_borderSTR: "{ border-bottom-width:1px; border-bottom-style:solid; border-bottom-color:black }", //-- divider between titlebar and content -> hbox border bottom
}
//-- standards titlebar_margin, content_margin
Object.defineProperties (ToolFrame.standards,{
titlebar_margin : { get: () => titlebar_margin, set: function (value) {
titlebar_margin=value;
this.titlebar_margin_start=value; this.titlebar_margin_end=value;
this.titlebar_margin_top=value;this.titlebar_margin_bottom=value;}, enumerable:true, configurable:false },
content_margin : { get: () => content_margin, set: function (value) {
content_margin=value;
this.content_margin_start=value; this.content_margin_end=value;
this.content_margin_top=value;this.content_margin_bottom=value;}, enumerable:true, configurable:false }});
//----------------- STATIC object ToolFrame standards -- END
//------------------------------------------------------------------------------------------------------------------------------------
} //-----> blockscope Class ToolFrame
/* ==================================================================================================================================
CLASS ToolFrame -- END
================================================================================================================================== */
let file=Gio.File.new_for_uri("https://i.ytimg.com/vi/fa5e_r6ZPoM/maxresdefault.jpg"),
image=Gtk.Image.new_from_pixbuf (GdkPixbuf.Pixbuf.new_from_stream(file.read(null),null));
new ToolFrame(fixed_container,20, 20, "http://www.iconeasy.com/icon/png/File%20Type/Software%20Files/Txt.png", "Text Viewer", "view text", new Gtk.Label ({label:"hello World\n Hello World Hello World"}));
ToolFrame.standards.toolframe_expanded_width=500;
ToolFrame.standards.toolframe_expanded_height=300;
ToolFrame.standards.buttons_width=24;
new ToolFrame(fixed_container,20, 60, "http://icons.iconseeker.com/png/fullsize/glaze/folder-image.png", "Image Viewer", "view image", image);
window.add(fixed_container);
window.set_title("Toolframe Example");
window.connect('destroy', () => { Gtk.main_quit(); });
window.set_size_request (740, 600);
window.show_all(); Gtk.main();
it was my fault.
To bring a widget on top of drawing action (zIndex) everytime a click happened inside the widget, i removed it from the Fixed container and add it at last child, this caused the strange action.
I now use Gtk.Overlay for the zIndex and every toolframe has its own Gtk.Fixed container as stage and is added to Gtk.Overlay.
I have a simplier example with Images. class MovingImages.
But how do you change the container(dashed box around the image) to a defined width and height smaller than the image?
#!/usr/bin/gjs
const Gtk= imports.gi.Gtk,
Gio= imports.gi.Gio,
GdkPixbuf= imports.gi.GdkPixbuf;
Gtk.init(null);
const window= new Gtk.Window(),
box= new Gtk.Box (),
overlay= new Gtk.Overlay();
box.add (new Gtk.Label ({ label:"Drag The Images!", hexpand:true, halign:3, valign:3 }))
//-- class MovingImage
{
var MovingImage=function (overlay, x, y, image_url)
{
let file=Gio.File.new_for_uri (image_url),
image=Gtk.Image.new_from_pixbuf (GdkPixbuf.Pixbuf.new_from_stream(file.read(null),null)),
cssProvider=new Gtk.CssProvider(), allocation;
this.overlay=overlay;
this.stage=new Gtk.Fixed ();
this.container=new Gtk.EventBox ();
this.image=image;
this.container.movingImage=this; //-- circular reference
cssProvider.load_from_data("* {border:1px dashed black;}");
this.container.get_style_context().add_provider (cssProvider,0);
this.container.add (image);
this.stage.put (this.container,x,y);
overlay.add_overlay (this.stage);
overlay.set_overlay_pass_through(this.stage,true);
this.container.connect ("button-press-event", container_button_down);
this.container.connect ("button-release-event", container_button_up);
}
MovingImage.prototype.instance_inAction=null;
let isMoving=false,
moveStart_allocation=null,
moveStart_event_coords=null,
container_button_down=function (container, event)
{
const event_coords=event.get_root_coords();
isMoving=true;
MovingImage.prototype.instance_inAction=container.movingImage;
moveStart_allocation=container.get_allocation ();
moveStart_event_coords={
x: Math.trunc(event_coords[1]),
y: Math.trunc(event_coords[2])};
container.movingImage.overlay.reorder_overlay (container.movingImage.stage,-1);
},
container_button_up=function ()
{
isMoving=false;
MovingImage.prototype.instance_inAction=null;
moveStart_allocation=null;
moveStart_event_coords=null;
},
container_move=function (window, event)
{
var event_coords, movingImage;
if (isMoving)
{
event_coords=event.get_root_coords();
movingImage=MovingImage.prototype.instance_inAction;
movingImage.stage.move (movingImage.container,
Math.trunc(event_coords[1]-moveStart_event_coords.x+moveStart_allocation.x),
Math.trunc(event_coords[2]-moveStart_event_coords.y+moveStart_allocation.y));
}
};
window.connect ("motion-notify-event", container_move);
}
//-- class MovingImage -- END
overlay.add (box);
window.add (overlay);
let image1=new MovingImage (overlay,20,20,"https://icons.iconarchive.com/icons/shrikant-rawa/animals/128/dog-icon.png"),
image2=new MovingImage (overlay,100,60,"https://icons.iconarchive.com/icons/martin-berube/square-animal/128/Bull-icon.png");
window.set_title("MovingImage Example");
window.connect('destroy', () => { Gtk.main_quit(); });
window.set_size_request (640, 480);
window.show_all(); Gtk.main();
to change the width and height, i have to connect to the "size-allocate" signal of the container and always overwrite the allocation
this.container.connect ("size-allocate", reallocate_container);
reallocate_container=function (container)
{
const allocation=container.get_allocation ();
allocation.width=50;
allocation.height=50;
container.set_allocation(allocation);
container.set_clip (allocation);
}
isnt there a way to set the width and height without this?
the complete change example:
#!/usr/bin/gjs
const Gtk= imports.gi.Gtk,
Gio= imports.gi.Gio,
GdkPixbuf= imports.gi.GdkPixbuf;
Gtk.init(null);
const window= new Gtk.Window(),
box= new Gtk.Box (),
overlay= new Gtk.Overlay();
box.add (new Gtk.Label ({ label:"Drag The Images!", hexpand:true, halign:3, valign:3 }))
//-- class MovingImage
{
var MovingImage=function (overlay, x, y, image_url)
{
let file=Gio.File.new_for_uri (image_url),
image=Gtk.Image.new_from_pixbuf (GdkPixbuf.Pixbuf.new_from_stream(file.read(null),null)),
cssProvider=new Gtk.CssProvider(), allocation;
this.overlay=overlay;
this.stage=new Gtk.Fixed ();
this.container=new Gtk.EventBox ();
this.image=image;
this.container.movingImage=this; //-- circular reference
cssProvider.load_from_data("* {border:1px dashed black;}");
this.container.get_style_context().add_provider (cssProvider,0);
this.container.add (image);
this.stage.put (this.container,x,y);
overlay.add_overlay (this.stage);
overlay.set_overlay_pass_through(this.stage,true);
this.container.connect ("size-allocate", reallocate_container);
this.container.connect ("button-press-event", container_button_down);
this.container.connect ("button-release-event", container_button_up);
}
MovingImage.prototype.instance_inAction=null;
let isMoving=false,
moveStart_allocation=null,
moveStart_event_coords=null,
container_button_down=function (container, event)
{
const event_coords=event.get_root_coords();
isMoving=true;
MovingImage.prototype.instance_inAction=container.movingImage;
moveStart_allocation=container.get_allocation ();
moveStart_event_coords={
x: Math.trunc(event_coords[1]),
y: Math.trunc(event_coords[2])};
container.movingImage.overlay.reorder_overlay (container.movingImage.stage,-1);
},
container_button_up=function ()
{
isMoving=false;
MovingImage.prototype.instance_inAction=null;
moveStart_allocation=null;
moveStart_event_coords=null;
},
container_move=function (window, event)
{
var event_coords, movingImage;
if (isMoving)
{
event_coords=event.get_root_coords();
movingImage=MovingImage.prototype.instance_inAction;
movingImage.stage.move (movingImage.container,
Math.trunc(event_coords[1]-moveStart_event_coords.x+moveStart_allocation.x),
Math.trunc(event_coords[2]-moveStart_event_coords.y+moveStart_allocation.y));
}
},
reallocate_container=function (container)
{
const allocation=container.get_allocation ();
allocation.width=50;
allocation.height=50;
container.set_allocation(allocation);
container.set_clip (allocation);
}
window.connect ("motion-notify-event", container_move);
}
//-- class MovingImage -- END
overlay.add (box);
window.add (overlay);
let image1=new MovingImage (overlay,20,20,"https://icons.iconarchive.com/icons/shrikant-rawa/animals/128/dog-icon.png"),
image2=new MovingImage (overlay,100,60,"https://icons.iconarchive.com/icons/martin-berube/square-animal/128/Bull-icon.png");
window.set_title("MovingImage Example");
window.connect('destroy', () => { Gtk.main_quit(); });
window.set_size_request (640, 480);
window.show_all(); Gtk.main();
Related
Is there a way to make an ion-modal-sheet to snap the header in Ionic V6
I'm currently developing an app using Ionic V6 and i'm figured out if it was possible to make an ion-modal-sheet snap the header when it fully swiped like the Uber eats application ? Thanks in advance for your answers and have a nice day !
For those who wondering how to do it, here's how i managed to get something similar: first of all i've added an id to my header (my ion-header is wrapped inside a component called header) if your header is always in the same page of where you're creating the modal, use a template reference variable instead of an id: <header id="getHeight" [title]="'Solutions disponibles'" [displayGoBack]="true"></header> then i've created a helper to: Get the height of the header (can be different between iOS and Android) Get the body height Use the body height and the header height to return the height my modal should have in px and in % export const getContentHeight = (): any => { const header = document.querySelector('#getHeight'); const body = document.querySelector('body'); if (body && header) { const percent = (100 - (header.clientHeight * 100 / body.clientHeight)); return { height: body.clientHeight - header.clientHeight, percent: percent } } } After that i've created another helper to change the visual aspect of my modal when breakpoint reach 0.9: export const setModalSheetContentHeight = (modal: HTMLIonModalElement, breakpoint: number, expand: boolean): any => { if (breakpoint >= 0.9 && !expand) { modal.classList.add('no-radius'); modal.setCurrentBreakpoint(1); expand = true; } else if (breakpoint <= 0.9 && expand) { modal.classList.remove('no-radius'); expand = false; } return expand; } The boolean "expand" avoid the helper to always set the modal breakpoint to 1. Then i've created a method that add some listeners on the modal: initModalListener(): void { let expand = false; // Define if the modal is fully expand const content = getContentHeight(); // Return an object with content height in px and in % addEventListener('ionModalWillPresent', () => { this.modal.setAttribute('style', `--height: ${content.height}px`); // Set the modal height before it shown to avoid visual bug }); addEventListener('ionBreakpointDidChange', (e: any) => { const breakpoint = e.detail.breakpoint; expand = setModalSheetContentHeight(this.modal, breakpoint, expand); }); } And after all of that i call this method inside of the method that create my modal: private async presentModal(): Promise<void> { this.modal = await this.modalCtrl.create({ component: YourModalComponent, breakpoints: [0.3, 0.65, 1], initialBreakpoint: 0.3, backdropBreakpoint: 1, showBackdrop: false, canDismiss: false, keyboardClose: true, id: 'modalSheet', } }); this.initModalListener(); await this.modal.present(); } This is a first shot so i'm sure that there is plenty of thing to adjust but for now, it work pretty well with Ionic 6.
How to make fixed header availabel at the focus an focusable element?
I have a fixed header, when user scroll to an offset 100px, a class add to header to make it fixed: fixed-header { position: fixed: top: 0, left: 0, right: 0, width: 100%; } $( window ).on( 'scroll', function() { if ( $( window ).scrollTop() >= 100 ) { navigation.classList.add('.fixed-header'); document.body.style.paddingTop = "100px" // prevent page jump when fixed header } else { navigation.classList.remove('.fixed-header'); document.body.style.paddingTop = "0" } }); On top of page, when user focus a link via tab key, I use focus event listener to check the fixed class in DOM to scroll focusable element to re-position it below fixed header: const elements = [...document.querySelectorAll( "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])" )]; header = document.body.querySelector('.header-top'); // Fixed header height: 100px const handleFocus = (e) => { console.log(header.classList.contains('.fixed-header')); // This will false if (header.classList.contains('.fixed-header')) { var windowScrollTop = $( window ).scrollTop(), focusableScrollTop = $(e.target).offset().top; if (focusableScrollTop - windowScrollTop < 100) { window.scrollTo({ top: focusableScrollTop - 100, behavior: 'smooth' }); } } }; for (const element of elements) { element.addEventListener('focus', handleFocus, { once: true }); } But focus callback handler fire before DOM repaint (when fixed class added to header), so it can not detect fixed class in DOM: Wrap focus handler inside requestAnimationFrame()', don't work, if I wrap focus handler inside setTimeout()' function with 300ms, it will detect correct fixed class, but cause page scroll jump (focusable element scroll up and down). for (const element of elements) { element.addEventListener('focus', setTimeout(() => { handleFocus() }, 300), { once: true }); } Any way to make header.classList.contains('.fixed-header') come true at the focusable link focused? I hope someone know this issue and help to fix this problem. Thank you very much.
Remove widget from Gtk.FlowBox
I'm trying to remove a Widget from a Gtk.FlowBox by destroying the widget, however there is a gray box left in place. Any idea how I can remove the gray box so that the adjacent widget falls into place after the removal of the widget. The following is how the widgets are packed: 1- Two Images(from pixbuf) and Label are added to a OverlayImage 2- Overlay Image is added to a EventBox 3- EventBox is added to the FlowBox I have tried the following methods: 1- Destroy the EventBox 2- Obtain and destroy all children of Overlay Image and then destroy OverLay Image and EventBox In both case, the widget is removed but a empty area stays in its place which is grey when clicked, but does nothing - see picture. How do I remove this empty space, so that the next widget automatically falls into the place where the widget was removed and the next widget falls into its place and so on. The code is available here and the "removeSelectedBooksFromLibrary" is the method which is removing the EventBox selected by the user from the FlowBox This is the code to add widgets from the FlowBox https://github.com/babluboy/bookworm/blob/master/src/bookworm.vala#L589 This is the code to remove widgets from the FlowBox https://github.com/babluboy/bookworm/blob/master/src/bookworm.vala#L512 Thanks in advance Here is a working example with the solution added, which removes the widget and its parent. public class FlowBoxIssue : Gtk.Window { public static int main (string[] args) { Gtk.init (ref args); FlowBoxIssue window = new FlowBoxIssue(); window.show_all (); Gtk.main (); return 0; } public FlowBoxIssue () { this.title = "FlowBox Issue"; this.window_position = Gtk.WindowPosition.CENTER; this.destroy.connect (Gtk.main_quit); this.set_default_size (800, 600); Gtk.FlowBox library_flowbox = new Gtk.FlowBox(); Gtk.Box library_mainbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 20); Gtk.ScrolledWindow library_scroll = new Gtk.ScrolledWindow (null, null); library_scroll.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); library_scroll.add (library_flowbox); Gtk.Overlay aOverlayImage1 = new Gtk.Overlay(); Gtk.EventBox aEventBox1 = new Gtk.EventBox(); Gtk.EventBox aEventBox2 = new Gtk.EventBox(); Gtk.EventBox aEventBox3 = new Gtk.EventBox(); try{ Gdk.Pixbuf aBookCover1 = new Gdk.Pixbuf.from_file_at_scale("cover.png", 200, 250, false); Gtk.Image aCoverImage1 = new Gtk.Image.from_pixbuf(aBookCover1); aOverlayImage1.add(aCoverImage1); Gtk.Label overlayTextLabel1 = new Gtk.Label("Label 1"); aOverlayImage1.add_overlay(overlayTextLabel1); aEventBox1.add(aOverlayImage1); library_flowbox.add (aEventBox1); }catch(Error e){ } Gtk.Overlay aOverlayImage2 = new Gtk.Overlay(); try{ Gdk.Pixbuf aBookCover2 = new Gdk.Pixbuf.from_file_at_scale("cover.png", 200, 250, false); Gtk.Image aCoverImage2 = new Gtk.Image.from_pixbuf(aBookCover2); aOverlayImage2.add(aCoverImage2); Gtk.Label overlayTextLabel2 = new Gtk.Label("Label 2"); aOverlayImage2.add_overlay(overlayTextLabel2); aEventBox2.add(aOverlayImage2); library_flowbox.add (aEventBox2); }catch(Error e){ } Gtk.Overlay aOverlayImage3 = new Gtk.Overlay(); try{ Gdk.Pixbuf aBookCover3 = new Gdk.Pixbuf.from_file_at_scale("cover.png", 200, 250, false); Gtk.Image aCoverImage3 = new Gtk.Image.from_pixbuf(aBookCover3); aOverlayImage3.add(aCoverImage3); Gtk.Label overlayTextLabel3 = new Gtk.Label("Label 3"); aOverlayImage3.add_overlay(overlayTextLabel3); aEventBox3.add(aOverlayImage3); library_flowbox.add (aEventBox3); }catch(Error e){ } Gtk.Button delete_button = new Gtk.Button.with_label("Delete Pix"); delete_button.clicked.connect (() => { //This is the line which resolved the issue - get the parent of the widget and destroy it and then destroy the widget aEventBox2.get_parent().destroy(); aEventBox2.destroy(); }); library_mainbox.pack_start(library_scroll, true, true, 0); library_mainbox.pack_end(delete_button, true, false, 0); this.add(library_mainbox); } }
Every time you add a child widget to a GtkFlowBox, the flow box widget will add an implicit child widget in between, for event handling and styling purposes — as the documentation for GtkFlowBox states: Although a GtkFlowBox must have only GtkFlowBoxChild children, you can add any kind of widget to it via gtk_container_add(), and a GtkFlowBoxChild widget will automatically be inserted between the box and the widget. This means that the line: library_grid.add(aEventBox); is really equivalent to: var flowbox_child = new Gtk.FlowBoxChild(); flowbox_child.add(aEventBox); library_grid.add(flowbox_child); If you want to remove a widget from a GtkFlowBox and you only keep a reference to the child you added, you will need to retrieve its parent and remove that from the GtkFlowBox: aEventBox.get_parent().destroy(); // or library_grid.remove(aEventBox.get_parent());
Differences btw Gtk.Table and Gtk.Grid
Althought Gtk.table is deprecated, I am getting better results with it, instead of the recommended Gtk.Grid. It is probably my mistake, but I couldn't find the problem. My aim is to create a Gtk window with a notebook at the top and two buttons below. These buttons should be horizontally aligned. My code with table, works as expected: uses Gtk class TestWindow : Window init // General characteristics of the window title = "Gtk Containers" default_height = 250 default_width = 250 window_position = WindowPosition.CENTER destroy.connect(Gtk.main_quit) // Now building the notebook var notebook = new Gtk.Notebook() var label1 = new Gtk.Label("Page one") var label2 = new Gtk.Label("Page two") var child1 = new Gtk.Label("Go to page 2 for the answer") var child2 = new Gtk.Label("Go to page 1 for the answer") notebook.append_page(child1, label1) notebook.append_page(child2, label2) // Now building the table var table = new Table(2,2,true) var button1 = new Gtk.Button.with_mnemonic("Button_1") var button2 = new Button.with_mnemonic("Button_2") // Attaching all elements into the table table.attach_defaults(notebook, 0,2,0,1) table.attach_defaults(button1, 0,1,1,2) table.attach_defaults(button2, 1,2,1,2) add(table) init Gtk.init (ref args) var test = new TestWindow () test.show_all () Gtk.main () However, the same code with the recommended Gtk.Grid gives me the two buttons without the notebook: uses Gtk class TestWindow : Window init // General characteristics of the window title = "Gtk Containers" default_height = 250 default_width = 250 window_position = WindowPosition.CENTER destroy.connect(Gtk.main_quit) // Now building the notebook var notebook = new Gtk.Notebook() var label1 = new Gtk.Label("Page one") var label2 = new Gtk.Label("Page two") var child1 = new Gtk.Label("Go to page 2 for the answer") var child2 = new Gtk.Label("Go to page 1 for the answer") notebook.append_page(child1, label1) notebook.append_page(child2, label2) // Now building the grid var grid = new Grid() var button1 = new Gtk.Button.with_mnemonic("Button_1") var button2 = new Button.with_mnemonic("Button_2") // Attaching all elements into the grid grid.attach(notebook, 0,2,0,1) grid.attach(button1, 0,1,1,2) grid.attach(button2, 1,2,1,2) init Gtk.init (ref args) var test = new TestWindow () test.show_all () Gtk.main () How to achieve the aim using Grid instead of Tables? Not asking for code, just a pointer.
gtk_table_attach_defaults() and gtk_grid_attach() operate differently. The official documentation in C points this out. Given thing.attach(widget, a,b,c,d) For GtkTable, the four numbers a, b, c, and d are the actual column and row numbers that the given edge of the widget should occupy. a is the left edge, b is the right edge, c is the top edge, and d is the bottom edge. For GtkGrid, a is the column and b is the row that the top-left corner of the widget should occupy, c is the number of columns wide the widget is, and d is the number of rows tall the widget is. Or in other words, table.attach_defaults(widget, left, right, top, bottom) is the same as grid.attach(widget, left, top, right - left + 1, bottom - top + 1) Hopefully some part of that explanation clears things up. The quick fix for your code would be grid.attach(notebook, 0,0,2,1) grid.attach(button1, 0,1,1,1) grid.attach(button2, 1,1,1,1)
For Gtk.Grid: attach(child, left, top, width, height) Parameters: child (Gtk.Widget) – the widget to add left (int) – the column number to attach the left side of child to top (int) – the row number to attach the top side of child to width (int) – the number of columns that child will span height (int) – the number of rows that child will span For Gtk.Table: attach(child, left_attach, right_attach, top_attach, bottom_attach, xoptions, yoptions, xpadding, ypadding) Parameters: child (Gtk.Widget) – The widget to add. left_attach (int) – the column number to attach the left side of a child widget to. right_attach (int) – the column number to attach the right side of a child widget to. top_attach (int) – the row number to attach the top of a child widget to. bottom_attach (int) – the row number to attach the bottom of a child widget to. xoptions (Gtk.AttachOptions) – Used to specify the properties of the child widget when the table is resized. yoptions (Gtk.AttachOptions) – The same as xoptions, except this field determines behaviour of vertical resizing. xpadding (int) – An integer value specifying the padding on the left and right of the widget being added to the table. ypadding (int) – The amount of padding above and below the child widget. Note: That the columns and rows are indexed from zero.
andlabs's explanation is correct, although his conversion adding 1 to grid.attach right and bottom values didn't work well to me. As I had to convert many of these values, I've prepared a python script to ease this process, HTH: import sys if len(sys.argv) < 2: raise Exception('Argument not provided') args = ''.join([str(x) for x in sys.argv[1:]]).split(',') if len(args) != 4: raise Exception('Please provide 4 attach arguments') left, right, top, bottom = map(lambda a: int(a), args) print("{}, {}, {}, {}".format(left, top, right - left, bottom - top))
Display values outside of pie chart in chartjs
When I hover on pie chart, the values are displayed in tooltip. However, I want to display values outside of pie chart. I want to make chart like this image: How to do this?
I was able to get something similar working using chart.js v2.3.0 using both the plugin API and extending chart types API. You should be able to take this as a starting point and tweak it to your needs. Here is how it looks after being rendered. Note, this requires digging deep into chart.js internals and could break if they change the way tooltips are positioned or rendered in the future. I also added a new configuration option called showAllTooltips to enable selectively using the plugin on certain charts. This should work for all chart types, but I am currently only using it for pie, doughnut, bar, and line charts so far. With that said, here is a working solution for the image above. Chart.plugins.register({ beforeRender: function (chart) { if (chart.config.options.showAllTooltips) { // create a namespace to persist plugin state (which unfortunately we have to do) if (!chart.showAllTooltipsPlugin) { chart.showAllTooltipsPlugin = {}; } // turn off normal tooltips in case it was also enabled (which is the global default) chart.options.tooltips.enabled = false; // we can't use the chart tooltip because there is only one tooltip per chart which gets // re-positioned via animation steps.....so let's create a place to hold our tooltips chart.showAllTooltipsPlugin.tooltipsCollection = []; // create a tooltip for each plot on the chart chart.config.data.datasets.forEach(function (dataset, i) { chart.getDatasetMeta(i).data.forEach(function (sector, j) { // but only create one for pie and doughnut charts if the plot is large enough to even see if (!_.contains(['doughnut', 'pie'], sector._chart.config.type) || sector._model.circumference > 0.1) { var tooltip; // create a new tooltip based upon configuration if (chart.config.options.showAllTooltips.extendOut) { // this tooltip reverses the location of the carets from the default tooltip = new Chart.TooltipReversed({ _chart: chart.chart, _chartInstance: chart, _data: chart.data, _options: chart.options.tooltips, _active: [sector] }, chart); } else { tooltip = new Chart.Tooltip({ _chart: chart.chart, _chartInstance: chart, _data: chart.data, _options: chart.options.tooltips, _active: [sector] }, chart); } // might as well initialize this now...it would be a waste to do it once we are looping over our tooltips tooltip.initialize(); // save the tooltips so they can be rendered later chart.showAllTooltipsPlugin.tooltipsCollection.push(tooltip); } }); }); } }, afterDraw: function (chart, easing) { if (chart.config.options.showAllTooltips) { // we want to wait until everything on the chart has been rendered before showing the // tooltips for the first time...otherwise it looks weird if (!chart.showAllTooltipsPlugin.initialRenderComplete) { // still animating until easing === 1 if (easing !== 1) { return; } // animation is complete, let's remember that fact chart.showAllTooltipsPlugin.initialRenderComplete = true; } // at this point the chart has been fully rendered for the first time so start rendering tooltips Chart.helpers.each(chart.showAllTooltipsPlugin.tooltipsCollection, function (tooltip) { // create a namespace to persist plugin state within this tooltip (which unfortunately we have to do) if (!tooltip.showAllTooltipsPlugin) { tooltip.showAllTooltipsPlugin = {}; } // re-enable this tooltip otherise it won't be drawn (remember we disabled all tooltips in beforeRender) tooltip._options.enabled = true; // perform standard tooltip setup (which determines it's alignment and x, y coordinates) tooltip.update(); // determines alignment/position and stores in _view tooltip.pivot(); // we don't actually need this since we are not animating tooltips, but let's be consistent tooltip.transition(easing).draw(); // render and animate the tooltip // disable this tooltip in case something else tries to do something with it later tooltip._options.enabled = false; }); } }, }); // A 'reversed' tooltip places the caret on the opposite side from the current default. // In order to do this we just need to change the 'alignment' logic Chart.TooltipReversed = Chart.Tooltip.extend({ // Note: tooltipSize is the size of the box (not including the caret) determineAlignment: function(tooltipSize) { var me = this; var model = me._model; var chart = me._chart; var chartArea = me._chartInstance.chartArea; // set caret position to top or bottom if tooltip y position will extend outsite the chart top/bottom if (model.y < tooltipSize.height) { model.yAlign = 'top'; } else if (model.y > (chart.height - tooltipSize.height)) { model.yAlign = 'bottom'; } var leftAlign, rightAlign; // functions to determine left, right alignment var overflowLeft, overflowRight; // functions to determine if left/right alignment causes tooltip to go outside chart var yAlign; // function to get the y alignment if the tooltip goes outside of the left or right edges var midX = (chartArea.left + chartArea.right) / 2; var midY = (chartArea.top + chartArea.bottom) / 2; if (model.yAlign === 'center') { leftAlign = function(x) { return x >= midX; }; rightAlign = function(x) { return x < midX; }; } else { leftAlign = function(x) { return x <= (tooltipSize.width / 2); }; rightAlign = function(x) { return x >= (chart.width - (tooltipSize.width / 2)); }; } overflowLeft = function(x) { return x - tooltipSize.width < 0; }; overflowRight = function(x) { return x + tooltipSize.width > chart.width; }; yAlign = function(y) { return y <= midY ? 'bottom' : 'top'; }; if (leftAlign(model.x)) { model.xAlign = 'left'; // Is tooltip too wide and goes over the right side of the chart.? if (overflowLeft(model.x)) { model.xAlign = 'center'; model.yAlign = yAlign(model.y); } } else if (rightAlign(model.x)) { model.xAlign = 'right'; // Is tooltip too wide and goes outside left edge of canvas? if (overflowRight(model.x)) { model.xAlign = 'center'; model.yAlign = yAlign(model.y); } } } });