Re: Scaffold API proposal



Hi Jeroen,

> > The ValueContainer interface:
> > 
> > One of the main components of the shell is the ValueContainer.  The
> > ValueContainer is an interface which can also be implemented by
> > plugins which want to "take over" certain subpaths of the main
> > hierarchy.
> > 
> > The shell's ValueContainer exposes a tree hierarchy where each of the
> > nodes can hold a single GValue.  Paths in the tree are represented by
> > strings, where each element of the path is separated by a special
> > character or sequence of characters (still undecided, but the slash
> > '/' will be used in the following examples).  The root of the
> > hierarchy is not accesible, and thus the initial slash in a given path
> > can be omitted.  Another way of looking at the model exposed by
> > ValueContainer is as nested hashes.
> 
> You say the initial slash can be omitted, yet you use this throughout
> this document. Contradiction? Seems a good idea to have an initial slash
> imo.

Well, my first thought was that since there's no such concept as the
current working directory, relative paths make no sense, and thus the
initial slash (root) can be omitted.  In any case, the root would
represent the root node of the current called ValueContainer.

On second thought, maybe we could force every implementor of the
ValueContainer to keep a reference to the shell and invoke the method in
the shell's ValueContainer whenever the first character of the path is a
slash.  I don't like this idea much since it complicates things a bit,
but it's an alternative worth studying.

> > An implementation of the ValueContainer interface can support
> > delegation.  This means, if a GValue which contains an object which in
> > turn implements the ValueContainer interface is installed at a given
> > path, all accesses to subpaths which have it as its parent will be
> > delegated to the installed object.  Delegation should be transparent.
> > 
> > E.g. an object A which realizes the ValueContainer interface is
> > installed at the shell's path '/Documents'.  An access to the path
> > '/Documents/Untitled0' the shell's object hierarchy will be delegated
> > to A (by using the ValueContainer interface) with a path of
> > '/Untitled0'.
> > 
> > There's no such thing as a relative path as in a filesystem.  A path
> > is an address, not a means of navigation.
> >   
> > 
> >   /* sets a given value in the repository; returns TRUE if the value
> >      could be set */
> >   gboolean set_value (const gchar *path, GValue *value, GError **error);
> > 
> >   /* tries to retrieve a value for a given path, returning TRUE if the
> >      value was set */
> >   gboolean get_value (const gchar *path, GValue *value, GError **error);
> > 
> >   /* removes a value from the repository */
> >   void unset_value (const gchar *path);
> 
> Might it not be better to call this clear_value?

clear_value sounds like setting to 0, the empty string or similar, not
removing the value from the container.  OTOH, g_value_unset() clears the
GValue.  Hmm... don't know :-/

> >   /* gets a list of strings with the set immediate subpaths */
> >   GSList *get_children (const gchar *parent);
> > 
> > 
> > The repository offered by the shell's ValueContainer is the place
> > where plugins should expose their services and go look for other
> > plugins services.  A standard hierarchy should be defined, which will
> > become part of the API, along with all the interfaces.
> > 
> >        [ UNDECIDED: what's the policy for replacing an existing value? ]
> 
> The current implementation emits a warning when a value is overwritten.
> This is supposed to show the developer when he's overwriting something
> he shouldn't. Don't know really whether this is a good solution.

We could provide some sort of destroy notify callback in set_value,
which would be called when the value is removed/replaced (something like
in g_object_set_data_full).  What do you think?

> > 
> > ----
> > The EventBus:
> > 
> > The EventBus interface provides a shell-wide notification mechanism.
> > Events emitted in the bus are considered of interest of several
> > plugins.  Notifications of lesser importance should be implemented
> > using the standard GSignal mechanism.
> > 
> > Group of events are selectable through patterns by the use of
> > wildcards (the g_pattern_spec_* API will be used for this).
> > 
> > While not enforced, it's expected that events will be namespaced.
> > E.g.: "Shell::ready", "Shell::quit", "Build::start", "Build::stop",
> > "Documents::change_active", etc..  The event names will be part of
> > Scaffold's API too.
> > 
> > Event emission is synchronous.
> > 
> > 
> >   /* registers a listener for a specific event pattern; use the returned
> >      id to unregister the listener */
> >   guint listen_event (const gchar *event_pattern, GClosure *closure);
> > 
> >   /* unregister a listener */
> >   void unlisten_event (guint id);
> 
> Why not call these add_listener and remove_listener?

Yeah, much better names.  I'll change that.

> >   /* emits an event in the bus */
> >   void emit_event (const gchar *event);
> > 
> > While presented as a separated interface, only the shell will
> > implement the event bus.
> > 
> > 
> > 
> > ---
> > The action UI merging interface UIActionTarget:
> > 
> > This interface will allow a plugin to merge action-type UI elements
> > (menus, toolbars).  This API mirrors the GtkUIManager API.
> > 
> > 	[ DECISION: should we have each visual component implement this
> > 	interface, or have a single implementation at the shell, and
> > 	somehow namespace the UI elements (popups)?
> > 
> > 	I tend to prefer offering this interface and have each
> > 	interested plugin implement it, since that allows greater
> > 	flexibility.  For example, in the project manager we could
> > 	have different popups depending on the row type and one place
> > 	to add common actions to all types.  The individual types
> > 	would be accessed as an interface in the shell's repository:
> > 	'/Project/targets', '/Project/sources', '/Project/groups',
> > 	while the common place through '/Project/all' (or similar).
> > 
> > 	If we decide to go the other way, we could eventually ditch
> > 	this interface and have a shell method which would return the
> > 	GtkUIManager. ]
> 
> I agree, the shell should only take care of the main window, toolbar,
> statusbar and docking widget. The rest should be taken care of by the
> respective plugins.

So we go with the first option (have each plugin implement this
interface)?

Btw, in case you didn't notice, I forgot about a statusbar API in my
proposal ;-)

> > 
> >   /* install an action group */
> >   void add_action_group (GtkActionGroup *group);
> > 
> >   /* remove an action group */
> >   void remove_action_group (GtkActionGroup *group);
> > 
> >   /* merge UI elements to the component's UI; the returned id is used
> >      for unmerging */
> >   guint merge_ui (const gchar *ui);
> > 
> >   /* unmerge a previously merged UI */
> >   void unmerge_ui (guint merge_id);
> > 
> > 
> > The shell will have a <menu> element and a <toolbar> element at a
> > minimum.  Plugins implementing the interface will have a <popup>
> > element.
> > 
> > 	[ UNDECIDED: how to register and use several toolbars ]
> 
> Multiple toolbars should be an implementation detail. The shell should
> allow a user to customize the way Actions(Groups) are displayed. It
> should also allow a user to create a second toolbar etc. (kinda like how
> epiphany handles toolbars).

This would rock.  I'll take a look at how epiphany does this and modify
the API if necessary.

> > 
> > ---
> > The FileOpen Service:
> > 
> > This service is offered by the shell in principle since it's way too
> > common and will probably be used by almost all plugins.
> > 
> > The concept is as follows: a plugin which can open a certain type of
> > document (selected by the mimetype) registers itself with the
> > service.  When an open request is made, the shell checks to see if it
> > has any registered provider and invokes the handler if so.  Otherwise,
> > it opens the file using Gnome's standard machinery (gnome_url_show()
> > or whatever).
> > 
> > Different plugins are expected to open different types of documents:
> > the document manager will open text files, the glade plugin will open
> > .glade files, the project manager will open .scaffold project files,
> > etc..
> > 
> > 	[ NOTE: Having the shell implement this service also solves
> > 	the problem of opening a file from the commandline; I don't
> > 	like the shell exposing the full command line in the value
> > 	repository ]
> > 
> > 
> >   /* the open handler signature; if the opened document can be
> >      manipulated the function should return a reference to the created
> >      document, NULL otherwise */
> >   typedef Document * (* FileHandler) (const gchar *uri, 
> >                                       gboolean read_only,
> >                                       GError **error);
> > 
> >   /* open a file and (if applicable) present it to the user; errors are
> >      indicated in the error parameter, and NULL is a valid result */
> >   Document *open_file (const gchar *uri, gboolean read_only, GError **error);
> > 
> >   /* register an open handler for a mime type */
> >   void register_file_handler (const gchar *mime_type, FileHandler handler);
> > 
> >   /* deregister an open handler */
> >   unregister_file_handler (FileHandler handler);
> 
> I'm not sure about this interface. I'd much rather see something like
> the Command interface from JBuilder (see opentools.pdf) where plugins
> can register command handlers. These would show up when you run scaffold
> --help as well. The document manager would be the "default command
> handler" and as such would get the command line passed as a parameter.

You're right.  This interface is not right.  At least it's not the shell
who should implement it.  More below...

> After startup, which plugins want to open a file? There's the
> document-manager itself which adds several GUI elements for opening
> files. Then there's the project-manager (when a user double-clicks on a
> file in the project tree), but that should be handled by using the
> document-manager interface, not the shell imo.

Well, virtually any plugin would want to open a file, even at startup
(e.g. the glade plugin).  Actually that was my rationale for making the
shell implement that interface and not a plugin.  But now I see a lot of
problems this would carry.

> There is the matter of specifying a project file on the command line
> (.scaffold), but that could be handled by the project-manager
> registering some kind of mime-type handler with the document-manager.

No, I like your command handler idea better.  I re-read that part from
the OpenTools document and it looks pretty good.

> I need to look at JBuilder's document manager before i can comment
> further on this though. JBuilder has a pretty nice document manager
> setup which allows for quite a lot of functionality & flexibility.

I think this interface could be implemented by the document manager as
it is.  This way, we make the document manager handle the basic file
operations, those common to all documents: new, open, close, save and
save as.  In this case the Document interface below would need to offer
the save method (and possibly a save_as method too) and a "modified" (or
dirty) property.  This quite simplifies things, since now the document
manager handles the "/Documents" value hierarchy and keeps the full
collection of documents himself.

Just occurred to me that we will probably need some sort of interface
for cut and paste in the document manager too.  Otherwise we might end
up with an Edit menu with several Cut, Copy and Paste entries, one for
each plugin capable of editing a document.

> > 
> > ---
> > The Document interface:
> > 
> > The document interface represents a single file opened by the
> > application.
> > 
> >   gchar *get_uri ();
> > 
> >   gchar *get_mime_type ();
> > 
> >   gboolean get_editable ();
> > 
> >   /* users of this method? */
> >   save ();

As stated above we would need this method now, and the user would be the
document manager.  Also, the signature should change to support save
as.  Either we,

  void save (gboolean save_as);

and make each plugin implement the dialog, or

  void save (const gchar *uri);

and a NULL implies the current name.  I like the first alternative
better.  Mainly because unless we add yet another property "untitled",
the plugin will need to implement the save dialog anyway.

> > 
> 
> See above. Need to read up on JBuilder docs before commenting.
> 
> > 
> > ---
> > The TextDocument interface:
> > 
> > Implemented by textual documents, allows direct manipulation of the
> > file contents.
> > 
> > Document regions are delimited by the point and the mark (ala emacs).
> > Cut and copy operations affect the region.  Insert occurs at the point
> > always.
> > 
> >   /* movement modifiers: a combination of one value from the following
> >      mask values */
> > 
> >   #define RELATIVE_POS_SHIFT 0
> >   #define STEP_SIZE_SHIFT    2
> > 
> >   /* relative position */
> >   #define FROM_CURRENT 0 << RELATIVE_POS_SHIFT
> >   #define FROM_START   1 << RELATIVE_POS_SHIFT
> >   #define FROM_END     2 << RELATIVE_POS_SHIFT
> > 
> >   /* steps */
> >   #define CHAR   0 << STEP_SIZE_SHIFT
> >   #define WORD   1 << STEP_SIZE_SHIFT
> >   #define LINE   2 << STEP_SIZE_SHIFT
> >   #define COLUMN 3 << STEP_SIZE_SHIFT
> > 
> >   void set_mark (gboolean swap_with_point);
> > 
> >   gboolean move_point (gint modifier, gint offset);
> > 
> >   gchar *get_text (gboolean cut);
> > 
> >   void insert_text (const gchar *text);
> > 
> >   gint get_position (gint modifier);
> > 
> 
> See above. Need to read up on JBuilder docs before commenting.

I read the OpenTools document and it offers almost the same
functionality.  I don't think we need more than this.  Don't forget
these are raw interfaces, which means we can have sugar wrappers for
common sequences or options.

> 
> > 
> > ---
> > ShellUI:
> > 
> > The ShellUI interface deals with visual components in the shell's
> > container (dock) and the preferences dialog.
> > 
> > 
> >   /* adds a widget to the shell's dock; returns FALSE if there already
> >      existed a widget with the same name */
> >   gboolean add_dock_widget (GtkWidget   *widget,
> >                             const gchar *name,
> >                             const gchar *title,
> >                             const gchar *pos_hint);
> 
> Don't forget a const gchar *stock_id parameter here :) Also, shouldn't
> the pos_hint be an int (enum)?

Right, of course :-)  The pos_hint is meant to be the name of a previous
dock item (to dock on top of it) or a placeholder name (we should
provide a few good defaults).

> >   /* adds a preference page to the preferences dialog; returns FALSE
> >      if there already existed a page with the same name */
> >   gboolean add_preferences_widget (GtkWidget *page,
> > 				   const gchar *name,
> > 				   const gchar *title,
> > 				   const gchar *category);
> 
> I'm thinking a const gchar *path parameter instead of the name/title &
> category implementation is better. The current way it does things to get
> the treeview in the prefs dialog filled kinda sucks.

I'm not sure... what about i18n issues with the path?

> >   /* removes a widget from the shell */
> >   void remove_widget (GtkWidget *widget);
> 
> Use the name parameter from the add call please. (or have a
> remove_widget_by_name call).

Not sure about this.  I think it's simpler to tell which widget exactly
you want to remove (either a preference or a dock item).  Imagine a text
document manager which adds one dock item per document (instead of
adding the document to a notebook as the current implementation).  I
know it's trivial to keep a copy of the name string in the document
object, but I think removing the object itself makes more sense. 
Anyway, a minor nit pick IMO.

> >   /* makes sure the widget is visible to the user */
> >   void present_widget (GtkWidget *widget);
> > 
> > 
> > 	[ UNDECIDED: The preferences dialog should be unique among the
> > 	application, but there might exist more than one shell at any
> > 	given time.  How do we deal with the preference pages? ]
> 
> Each shell has a preferences dialog. When a new shell is created, new
> plugin instances are created as well, which register pref pages. I don't
> see a problem here. (synchronization between shells happens via gconf
> notification)

Yes, but having a preferences dialog per shell sucks IMHO.  I'd like to
have just one.  Apart from the dock configuration, all preferences
should affect all windows equally.

> 
> > 	[ UNDECIDED: present_widget() should open the preferences
> > 	dialog if the widget is a preference page? ]
> 
> Better to have a separate call for this, using the path parameter
> probably. present_preference_page (const gchar *path);

Why another method?

> 
> > 
> > 
> > ---
> > The data UI merging interface UIDataTarget:
> > 
> > Plugins which have visual elements which show information that can be
> > extended by other plugins (e.g. the vcs plugin can add information to
> > the project manager plugin) can implement this interface, which is
> > roughly a stripped down GtkCellLayout.
> > 
> > GtkCellRenderers can be packed in groups.  Groups have an id and can
> > have a title.  The main difference with the GtkCellLayout interface is
> > the absence of a GtkTreeModel/GtkTreeIter pair to feed the set data
> > function.  Instead a ValueContainer and a path into it is given.
> > 
> > 
> >   /* set data function for the renderers */
> >   typedef void (* CellDataFunc) (CellRenderer *cell, 
> > 	 		         ValueContainer *container, 
> > 			         const gchar *path, 
> > 			         gpointer user_data);
> > 
> >   /* create a new group of renderers to pack cells into */
> >   guint add_group (const gchar *group_title);
> > 
> >   /* pack a cell into the given group using the func to fill the data */
> >   void pack_cell (guint cell_group,
> >                   GtkCellRenderer *cell,
> >                   gboolean expand,
> >                   CellDataFunc func,
> >                   gpointer user_data);
> > 
> >   /* destroy the cell group */
> >   void remove_group (guint id);
> > 
> > 
> > 	[ NOTE: I'm not really convinced with this interface, but I
> > 	can't find another example (besides the vcs<->project
> > 	interaction) which would use it. ]
> 
> I don't really know what to think of this. Looks all very strange to me
> :) I think this can wait until we need to cross that bridge btw
> (implement a vcs plugin).

Ok, agreed :-)  Another alternative to explore is something along the
lines of nautilus extensions, but that implies a bit more work.

> > 
> > 
> > ---
> > BuildTarget:
> > 
> > The interface presented by a buildable and (possibly) executable
> > project target.
> > 
> > 	[ FIXME: this needs a lot of thought ]
> > 
> >   GList *list_sources ();
> > 
> >   add_source (const gchar *uri);
> > 
> >   remove_source (const gchar *uri);
> > 
> >   gboolean is_executable ();
> 
> Yeah, need more thought. Not a high priority imho though (for now). We
> need to get the basics right first.

Right.

> 
> > 
> > ---
> > [ FIXME: Design the project interface ]
> > [ FIXME: Design a reflection interface to discover runtime interfaces ]
> 
> This is an important one.

I'm not really sure what will be needed for this.  For properties we can
use GObject's properties, or even implement it using our own
GParamSpecPool and providing a simple "list" method which would return
an array of GParamSpec.  I'm sure methods can't be much more
complicated.  Suggestions?

> 
> > [ FIXME: Other interfaces? ]
> > 
> > 
> > 
> > 2. STRING INTERFACE (repository hierarchy and event names)
> > ==========================================================
> > 
> > Whatever the implementation of the above interfaces, there must be a
> > standard way for plugins to look for specific well-known interfaces.
> > As stated above, the value repository is the place for plugins to
> > publish their services.  The missing part is how to name the services.
> > 
> > The following paths expressed from the shell's repository should lead
> > to the specified services:
> > 
> > /Documents
> > 	Text documents collection (each child of this node implements
> > 	the TextDocument interface)
> > 
> > /Documents/Current
> > 	The currently (or last) active text document
> > 
> > /Project
> > 	The project interface and starting point to access all project
> > 	elements such as targets and sources
> 
> How do we deal with project groups? Let's say we have a Scaffold project
> group which contains gdl, gtksourceview, glimmer, gnome-build and the
> scaffold project in it? Probably a bad example to start with, but you
> get the idea.
> 
> So perhaps it should be /Projects and /Projects/Current.

Well, we never spoke about having more than one project open at a time. 
Currently you could do that by having several shell windows open
simultaneously.

OTOH, I can see the benefit of having more than one project open at
time, and even have build dependencies among them.  One solution I've
just figured out would be to have a meta project backend in
gnome-build.  I'm not entirely sure it's possible, but at the same time
I can't see any impediment.  The more I think of it, the more I like the
idea :-)

> > /Project/Targets
> > 	Targets collection
> > 
> > /Session
> > 	Session saved data.  This should present a ValueContainer
> > 	interface.  Only serializable data should be allowed here.
> > 
> > 
> > The following are standard event names:
> > 
> > Shell::Ready
> > 	The shell has finished loading the plugins and it's ready to
> > 	begin operations
> > 
> > Shell::Quit
> > 	The user wants to exit the application
> > 
> > Session::Loaded
> > 	The session plugin has finished loading the session data,
> > 	which is now available at /Session
> > 
> > Session::Save
> > 	The session plugin requests that all interested parties save
> > 	session data to /Session
> > 
> > 
> > 
> > 3. STARTUP AND SHUTDOWN SEQUENCE
> > ================================
> > 
> > Note: the session management is implemented by a plugin which also has
> > the capability of opening session files (i.e. it registers an open
> > handler).  The session data itself is sustained and accessed by a
> > ValueContainer interface.
> 
> Should it really register an open handler? Do we want users to be able
> to open a session file through the File->Open menuitem? IMHO it would be
> better to have explicit Load Session and Save Session actions somewhere.

Yes, you're right.  See my comments above about the open service.

> > The creation of a new shell involves the following operations and
> > events:
> > 
> > - the shell is created
> > - all the tools in the shell toolset are loaded in order
> > - the shell opens all files given in the command line
> > - the Shell::Ready event it emitted
> >   - if there is a session plugin
> >     - on emission of the Shell::Ready event, it loads the (default or
> >       command line specified) session file and emits Session::Loaded
> >     - all interested plugins can now load the session settings by
> >       registering to listen to the Session::Loaded event
> > 
> > On shutdown (when the user quits the application or closes the shell
> > window), the following sequence takes place:
> > 
> > - the event Shell::Quit is emitted
> >   - all plugins can perform shutdown operations, including asking the
> >     user to save unsaved data
> >   - if there is a session plugin
> >     - emits the Session::Save event
> >       - all registered plugins save session data to /Session
> >     - save the session file and perform shutdown operations
> > - all tools are unloaded in reversed order
> > - the shell is destroyed
> 
> I have one big issue with this: current implementation uses a shutdown
> callback in ScaffoldTool which has a return value of gboolean. This
> allows a plugin to return FALSE if it doesn't want the shell to quit (in
> case a user answer's "Cancel" on the question of "Save documents?") How
> is this handled in your scenario?

Hrm... I hadn't thought about that :-)  First solution I can think of is
have a shell method "cancel_quit" a plugin could invoke when it receives
the Shell::quit event, but I'm sure there must be a better and more
elegant solution.

> 
> > 
> > 	[ FIXME: what about loading a session file in the middle of
> > 	another session? We need an event to signal all plugins to
> > 	reset their contents. ]
> 
> Why not use Session::Loaded for this?

Depending on how we handle files opened from the command line, this
could work.  The problem is differentiating between the Session::Loaded 
at startup and the Session::Loaded when I choose to load a different
session.

> The proposal looks quite good overall. I'm anxious to see your
> reflection proposal though because that's quite important if you want to
> move away from having to include header files to use the various
> interfaces.

As I told you before, I'm not quite sure what's needed.  Properties are
easy: use GObject system, have a simple interface get/set/list or better
yet, reuse the ValueContainer interface.

Methods can't be very difficult.  Heck, we could even use GSignal for
that.  Check this methods: g_signal_list_ids, g_signal_name,
g_signal_query, g_signal_emit_by_name, etc..

> On a side note: what do you think about using "sde_" as a prefix for
> these methods? (sde stands for Scaffold Development Evironment) It's a
> whole lot shorter than prefixing everything with "scaffold_". Same goes
> for classes. Plugins would only use the "s" part of "sde". So
> project-manager would use the prefix "spm" (Scaffold Project Manager).
> Just a thought though.

Looks good.

Regards,
Gustavo






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