This Bugzilla instance is deprecated, and most Eclipse projects now use GitHub or Eclipse GitLab. Please see the deprecation plan for details.
Bug 295524 - Need to devise a strategy for merging user changes to the UI with the application's UI definition
Summary: Need to devise a strategy for merging user changes to the UI with the applica...
Status: RESOLVED FIXED
Alias: None
Product: e4
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 1.0   Edit
Hardware: All All
: P3 normal (vote)
Target Milestone: 1.0 M3   Edit
Assignee: Remy Suen CLA
QA Contact: Remy Suen CLA
URL:
Whiteboard:
Keywords:
Depends on: 299038
Blocks:
  Show dependency tree
 
Reported: 2009-11-18 17:37 EST by Remy Suen CLA
Modified: 2010-01-14 13:58 EST (History)
8 users (show)

See Also:


Attachments
Model merging patch v1 (112.26 KB, patch)
2009-11-26 12:53 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v2 (123.56 KB, patch)
2009-11-27 16:34 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v3 (143.50 KB, patch)
2009-11-28 20:05 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v4 (151.20 KB, patch)
2009-11-28 22:06 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v5 (197.34 KB, patch)
2009-11-29 15:56 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v6 (218.42 KB, patch)
2009-11-29 22:01 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v7 (227.67 KB, patch)
2009-12-01 16:02 EST, Remy Suen CLA
no flags Details | Diff
Model merging patch v8 (251.82 KB, patch)
2009-12-02 14:08 EST, Remy Suen CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Remy Suen CLA 2009-11-18 17:37:38 EST
In an Eclipse-based application, there are currently three "layers" of definition about the user interface.

1. The application's default presentation and setup

This would be synonymous with the perspective defined in the factory or the e4 XMI file.

2. Contributed customizations from external plug-ins

New plug-ins add views and leverage the perspectiveExtensions extension point.

3. The user's own bidding

The user can move views around and show/hide them as they please.

----------

The merged...blob...of all of the above is the final result of what's presented to the user. At the moment, the 3.x story for this is not very good. Anyone that's ever used the perspectiveExtensions extension point has probably hit bug 48060 (such as yours truly). We will need to provide a _predictable_ story for this in e4. Whether it is "good" or "bad" is a different story but the exact behaviour should be documented and well defined.

Since we use EMF at the moment for serializing our models, we will begin by testing the o.e.emf.ecore.change.util.ChangeRecorder class to see if it can do what we want by recording ("all") the deltas that has been caused (by user actions).
Comment 1 Remy Suen CLA 2009-11-19 08:24:52 EST
The ChangeRecorder (unsurprisingly) batches all the changes instead of recording every single detail.

That is, if your MPart's name was originally "abc" and you changed it five bajillion times and finally settled on "def", only one change would ever be recorded and the change would simply record that the name was originally "abc", it does not seem to have information about the fact that the part's name is now "def".

Moving a part from one stack to another produces three changes.

stack1.getChildren().remove(part1);
stack2.getChildren().add(part1);

1. stack1's children has changed, one part was removed.
2. part1's parent has changed.
3. stack2's children has changed, one part was added.

The fact that stack1's children count went down by one and stack2's went up by one is not actually recorded. I suppose we could perform a diff between the recorded and existing list and figure out what exactly happened.
Comment 2 Remy Suen CLA 2009-11-19 09:04:33 EST
(In reply to comment #1)
> The fact that stack1's children count went down by one and stack2's went up by
> one is not actually recorded. I suppose we could perform a diff between the
> recorded and existing list and figure out what exactly happened.

Note that since part stacks generally don't have an id (although they can since MPartStack ultimately extends MApplicationElement), I don't think we can really persist these diffs in a meaningful manner.
Comment 3 Eric Moffatt CLA 2009-11-19 09:30:07 EST
Note that the idea of making ui elements like stacks and sashes that get arbitrarily created or destroyed by the UI (as a result of user changes) have id's is dubious at best. The 3.6 approach of naming a particular stack 'left' and using that id to extend the perspective is dubious at best.

We can't properly manage perspectiveExtensions at any time other than after a 'reset' because that's the only time we can be sure that the 'left' stack exists...

Note that for save/restore purposes EMF already (I believe) supports its own unique ids (for managing refs).
Comment 4 Remy Suen CLA 2009-11-19 09:36:39 EST
(In reply to comment #3)
> Note that for save/restore purposes EMF already (I believe) supports its own
> unique ids (for managing refs).

Did you mean something like this?

<children xsi:type="application:PartStack" activeChild="//@children.0/@children.0/@children.1/@children.0">
Comment 5 Remy Suen CLA 2009-11-19 16:10:39 EST
(In reply to comment #4)
> <children xsi:type="application:PartStack"
> activeChild="//@children.0/@children.1">

If this is the format you speak of then I do not think it is sufficient. If I
persist that to disk and the application's XMI changes the source position of
the part then I cannot apply this delta.

Consider the case below...

Program defined like so...

MWindow
-MPartStack (//@children.0)
--ViewA (//@children.0/@children.0)
--ViewB (//@children.0/@children.1)
-MPartStack (//@children.1)
--ViewC (//@children.1/@children.0)

...and the user changes it like so...

MWindow
-MPartStack (//@children.0)
--ViewB (//@children.0/@children.0)
-MPartStack (//@children.1)
--ViewA (//@children.1/@children.0)
--ViewC (//@children.1/@children.1)

...so the delta would be...

//@children.0/@children.0 -> //@children.1/@children.0

Now if the program is switched to be laid out like so...

MWindow
-MPartStack (//@children.0)
--ViewB (//@children.0/@children.0)
--ViewA (//@children.0/@children.1)
-MPartStack (//@children.1)
--ViewC (//@children.1/@children.0)

...and we read the new definition and applied the delta, we would get...

MWindow
-MPartStack (//@children.0)
--ViewA (//@children.0/@children.1)
-MPartStack (//@children.1)
--ViewB (//@children.0/@children.0)
--ViewC (//@children.1/@children.0)

If there were unique ids for "everything" then we could just define what
elements an element container has and the rest should fall into place. Forcing
everything in the model to have a unique id doesn't make for a very pleasant
development experience though I suppose. And of course, if the ids change the delta's not going to be applicable anymore.
Comment 6 Paul Webster CLA 2009-11-20 08:43:25 EST
(In reply to comment #1)
> That is, if your MPart's name was originally "abc" and you changed it five
> bajillion times and finally settled on "def", only one change would ever be
> recorded and the change would simply record that the name was originally "abc",
> it does not seem to have information about the fact that the part's name is now
> "def".

That sorta makes sense (if you think it's a transaction).  It's only the starting point that matters, as you will already have the end point.

So do they expect you to create multiple change recorders "per transaction"?  Although I guess I don't see how you could take the starting model and replay the change recorder and get the end point, then, unless you take the end model and the change and there's some "create reverse change" ...

PW
Comment 7 Remy Suen CLA 2009-11-20 12:34:27 EST
(In reply to comment #6)
> So do they expect you to create multiple change recorders "per transaction"? 
> Although I guess I don't see how you could take the starting model and replay
> the change recorder and get the end point, then, unless you take the end model
> and the change and there's some "create reverse change" ...

Don't quite follow here but this is what I have right now.

MWindow window = MApplicationFactory.eINSTANCE.createWindow();
window.setId("windowId");
window.setName("name");

// begin recording changes
ModelReconciler reconciler = createModelReconciler(window);

// change occurred
window.setName("name2");

// serialize all changes thus far
Object serializedState = reconciler.serialize();

window = MApplicationFactory.eINSTANCE.createWindow();
window.setId("windowId");
window.setName("name");

// apply the delta, i.e. "name" -> "name2"
reconciler.applyDeltas(window, serializedState);

// verify
assertEquals("name2", window.getName());

This is what I have serialized right now.

<changes>
  <MWindow id="windowId">
    <name name="name2"/>
  </MWindow>
</changes>

As hinted at above, my current implementation relies on the fact that _everything_ has a unique id.
Comment 8 Remy Suen CLA 2009-11-20 14:51:26 EST
Some abstract cases to consider:

-----------

Application v1.0 part name defined as "name".
User changes name to "customName".
Application v2.0 part name defined as "name2".

Result: part's name is "customName" / USER wins

-----------

Application v1.0 part has a part in a stack.
User changes an attribute of the part.
Application v2.0 part no longer shows up in the stack by default.

Result: part is not shown in the stack / APPLICATION wins

-----------

Application v1.0 part has a part in a stack.
User hides the part so it is no longer in the stack.
Application v2.0 part has a change in one of its attributes.

Result: part is not shown in the stack / USER wins

-----------

Application has three parts in a stack, A, B, and D.
User hides part B.
Application now has four parts in a stack, A, B, C, and D.

Result: stack should show A, C, and D / USER wins
Comment 9 Remy Suen CLA 2009-11-26 12:53:01 EST
Created attachment 153189 [details]
Model merging patch v1

Only a very small group of changes are recorded at the moment. For example, stuff like names, icon URIs, part visibility, part positions, keybinding/command references, window positions and sizes are recorded. There are a 198 tests at the moment which really just barely scratches the surface of the ways a user may interact with the model. The vast majority of these tests are isolated and merely verifies one single change. The "play-by-play" tests are captured in ModelReconcilerScenarioTest. The tests should be completely interchangeable with another implementation of the ModelReconciler class so it should be possible for clients to provide their own serialization mechanism.
Comment 10 Remy Suen CLA 2009-11-26 20:39:36 EST
We can do diffs of models with EMF Compare but I haven't quite yet got a handle on how to perform the merge. We can diff the original model with the user's copy for the deltas but then I'm not sure how we can get the deltas mapped onto the new model correctly. It sounds to me like it's ripe for the disaster described in comment 5. I'll have to look into EMF Compare's 3-way merge capabilities as the notion of an ancestor model should hopefully provide sufficient context for a predictable merged result.
Comment 11 Remy Suen CLA 2009-11-27 10:39:20 EST
At the moment, EMF Compare is able to pass 132/224 tests if the model has no ids (for anything) and 137/224 if the model has ids (for everything). It's not clear to me at the moment why there is so little gain but there you have it.

Note that I did not manually intervene to produce the results above. I merely called the APIs and ran the tests.

MatchModel match = MatchService.doMatch((EObject) localModel,
    (EObject) newModel, (EObject) ancestorModel, null);
DiffModel diffModel = DiffService.doDiff(match);
for (DiffElement diff : diffModel.getDifferences()) {
  MergeService.merge(diff, true);
}
Comment 12 Remy Suen CLA 2009-11-27 16:34:40 EST
Created attachment 153281 [details]
Model merging patch v2

Just to be clear, the tests don't pass.

Going forward, I'll be taking a no-id policy approach although it is prone to failure.

With regards to using indices to identify parts, the problem becomes complicated once you move a part from one stack to another because now it is no longer sufficient to just id it by its index because that index is invalid since its position has changed from the original model. The part's original parent would have to be found first and then we'd have to recursively go up the chain and check all of those parent containers (in case any of those had its parent changed). :|
Comment 13 Remy Suen CLA 2009-11-28 20:05:15 EST
Created attachment 153302 [details]
Model merging patch v3

(In reply to comment #12)
> With regards to using indices to identify parts, the problem becomes
> complicated once you move a part from one stack to another because now it is no
> longer sufficient to just id it by its index because that index is invalid
> since its position has changed from the original model. The part's original
> parent would have to be found first and then we'd have to recursively go up the
> chain and check all of those parent containers (in case any of those had its
> parent changed). :|

Implementing this has not been very fun. We're now at 127/228. The current failures are all related to keybindings.

I'll have to refactor all reference-related operations later to batch them up so they can all be run in one go. Processing them one at a time is a recipe for disaster because (I'm pretty sure) the current lookup strategy will fail if I start moving something that others depend on. I'm pretty sure it will cause a cascade of failures because of the outdated information.
Comment 14 Remy Suen CLA 2009-11-28 22:06:09 EST
Created attachment 153304 [details]
Model merging patch v4

237/237 tests pass.
Comment 15 Remy Suen CLA 2009-11-29 15:56:53 EST
Created attachment 153310 [details]
Model merging patch v5

508/508 tests pass (tests have doubled to consider the case where all MApplicationElements have ids).

MPart's tool bar (getToolbar()) and menus (getMenus()) don't seem to return the MPart itself when eContainer() is invoked on them. I've added extra code to consider these two cases.
Comment 16 Remy Suen CLA 2009-11-29 22:01:41 EST
Created attachment 153313 [details]
Model merging patch v6

Added support for preserving MGenericTile/MTrimContainer horizontal orientations. Also placed some simple tests for MHandledItem, will need to buff it up some more.

Non-exhaustive list of unimplemented features:
-sash weights are not considered, I think Eric said he was going to change this to not use a List
-child creation handling, if an application has one window then another one is created "in front" of the original, the id lookup will fail since it'll look for window0 which is now actually window1, no idea how to solve this at the moment
-error reporting to notify of failed merges
Comment 17 Remy Suen CLA 2009-12-01 13:31:34 EST
(In reply to comment #16)
> -child creation handling, if an application has one window then another one is
> created "in front" of the original, the id lookup will fail since it'll look
> for window0 which is now actually window1, no idea how to solve this at the
> moment

So I kind of tried to explain this in words today at the UI call. Perhaps composing it in words instead of being forced to blabber on-the-spot will produce better results. ;)

Say you have the 'Problems' view and 'Console' in a stack and their parent stack container is currently identified by the (generated) id partContainer.0. Now if you were to select one of the views and drag it towards the left area of the stack and release, you would now be able to see both views in their own part containers. The problem of course is that the id information is now stale because partContainer.0 now refers to the newly created container (on the left) instead of the original (on the right).

I suppose one solution here might be to "force" these user generated elements to have a UUID assigned to it. I presume the renderer will be the one that handles the DND action and subsequently constructs the model element so there's nothing stopping us from assigning a random UUID to it for bookkeeping purposes.
Comment 18 Remy Suen CLA 2009-12-01 16:02:28 EST
Created attachment 153537 [details]
Model merging patch v7

(In reply to comment #16)
> -sash weights are not considered, I think Eric said he was going to change this
> to not use a List

Threw in a few tests and a basic implementation for this (since it'll be canned when the "weights" attribute is rewritten, see bug 296478).

I'm going to do some testing and then I hope to actually commit to HEAD...after I speak with Paul to get the build sorted out, that is. ;)

> -error reporting to notify of failed merges

Returning an IStatus seems like a good way to do this in my opinion. Though it got me thinking that maybe the applyDeltas(Object, Object) method should actually return an array/collection of "operations" that can be applied one-by-one (instead of having the delta application be performed in the applyDeltas method). Then the client can actually pick and choose what to apply and what not to apply, some food for thought I suppose...
Comment 19 Remy Suen CLA 2009-12-01 17:16:03 EST
(In reply to comment #17)
> I suppose one solution here might be to "force" these user generated elements
> to have a UUID assigned to it. I presume the renderer will be the one that
> handles the DND action and subsequently constructs the model element so there's
> nothing stopping us from assigning a random UUID to it for bookkeeping
> purposes.

I talked to Boris about this and he was wondering if there was an EMF way to do it. It sounds suspiciously like bug 161599.
Comment 20 Remy Suen CLA 2009-12-02 14:08:01 EST
Created attachment 153639 [details]
Model merging patch v8

Just committed this to HEAD. If the demos starts going crazy, then you should be able to just switch the ResourceHandler.RESTORE_VIA_DELTAS flag to 'false' to get the regular behaviour back. Note that while it's 'true', only the deltas are persisted to disk. So the first time you flick it to 'true', it will be loading the original XMI and not the version you "last opened".
Comment 21 Remy Suen CLA 2009-12-02 14:09:40 EST
(In reply to comment #20)
> So the first time you flick it to 'true'

That should be 'false'. ~_~
Comment 22 Yves YANG CLA 2009-12-04 06:10:53 EST
Why don't use the EMF changer Recorder? It provides a transaction concept to valid whole operations or nothing. It is the solution we'll use for PMF.
Comment 23 Remy Suen CLA 2009-12-04 06:27:25 EST
(In reply to comment #22)
> Why don't use the EMF changer Recorder? It provides a transaction concept to
> valid whole operations or nothing. It is the solution we'll use for PMF.

Yves, that is what I'm using at the moment. However, it doesn't capture a sufficient amount of information because it just records the state of the application before anything happened. This is not sufficient for more complex operations.
Comment 24 Remy Suen CLA 2009-12-07 12:33:18 EST
With the org.eclipse.emf.ecore.extension_parser extension point, we can hook an EMF Factory implementation for the XMI. This will enable us with the ability to provide an identifier for the EMF objects via overiding XMLResource's setID(EObject, String) and getID(EObject) methods. By default, we can tell EMF to use UUIDs by overriding useUUIDs() to return 'true'.

One issue I've discovered with the "default" behaviour is that if I remove an object, say, I have a window and I remove a part, the mapping between that part and its id is lost. This presents a major problem when I try to do a diff to see which part was removed because I can no longer identify said removed part. We can solve this problem if we spin our own thing and keep these entries around but it sounds like a memory leak waiting to happen to me.
Comment 25 Remy Suen CLA 2009-12-09 11:47:17 EST
(In reply to comment #24)
> With the org.eclipse.emf.ecore.extension_parser extension point, we can hook an
> EMF Factory implementation for the XMI.

This requires a file extension. Currently, we use XMI but we obviously don't want to hijack everyone's XMI files. What file extension should we use? e4x? exmi? e4xmi?
Comment 26 Paul Webster CLA 2009-12-09 11:54:23 EST
(In reply to comment #25)
> This requires a file extension. Currently, we use XMI but we obviously don't
> want to hijack everyone's XMI files. What file extension should we use? e4x?
> exmi? e4xmi?

I like e4xmi ... not likely to clash with something else.

PW
Comment 27 Remy Suen CLA 2009-12-10 10:13:16 EST
I've added code to use the e4xmi resource factory. The reconciler has been _disabled_ for the time being since our model files all use the xmi file extension at the moment.

The constants are now retrieved from the EMF features so we should be able to react to model changes much easier. I've also made a bug fix where the code would inadvertently alter the model during delta construction, yikes! :/
Comment 28 Yves YANG CLA 2009-12-10 10:27:16 EST
(In reply to comment #25)
> (In reply to comment #24)
> > With the org.eclipse.emf.ecore.extension_parser extension point, we can hook an
> > EMF Factory implementation for the XMI.
> This requires a file extension. Currently, we use XMI but we obviously don't
> want to hijack everyone's XMI files. What file extension should we use? e4x?
> exmi? e4xmi?

Thanks, this could be the solution to read/write XWT for EMF.
Comment 29 Remy Suen CLA 2009-12-10 18:38:06 EST
Just hit a roadblock today, consider the following...

1. Window has a stack, originalStack, with parts A, B, and C.

window
-originalStack
--A
--B
--C

2. User adds a stack in front, userStack, and puts part A in it.

window
-userStack
--A
-originalStack
--B
--C

3. A new version of the program comes out with a new layout.

window
-originalStack
--A
--B
-newStack
--C

4. The expected merge result (I suppose?) would be the following.

window
-userStack
--A
-originalStack
--B
-newStack
--C

The current 3-way merge algorithm does not handle this case properly.

Original: A, B, C
User: B, C
New: A, B

Expected: B
Actual: B, C

In this case it would merely be the intersection of all three of these which I suppose doesn't really sound like much of a "merge". I suppose in general one would expect the outcome to be B and C (it is how I envisioned and implemented it and is naturally the reason why the actual outcome is B and C at the moment) but this only makes sense if the user had actually _specifically_ set B and C to be there.

Let's consider the problem from a different angle...

Original: A, B, C
User: -A
New: -C

(A + B + C) - A - C

Expected: B
Actual: B, C

...now it makes perfect sense for B to be the last man standing! Currently, I merely record the original list (A, B, C) and the "end" list upon shutdown (B and C). It seems like I need more metadata in my delta to capture this better...
Comment 30 Remy Suen CLA 2009-12-11 16:19:01 EST
What would the following merge into?

Original:
window
-stack
--partA

User:
window
-stack
--partA
--partB

New:
window
-stack
--partA
--partC

Would it be A, B, C? Would the user intend B to follow A? Or would it be A, C, B, where the user intended B to be the final part? I would argue that A, B, C makes sense on the basis that new parts should get thrown at the end...but then maybe C isn't actually a "new" part, ooo...

This list of "what goes where" is going to grow exponentially, fast, as I draft up more scenarios so I'm starting to wonder what the _general_ strategy/contract should be with these cases?
Comment 31 Remy Suen CLA 2009-12-14 08:13:42 EST
(In reply to comment #30)
> This list of "what goes where" is going to grow exponentially, fast, as I draft
> up more scenarios so I'm starting to wonder what the _general_
> strategy/contract should be with these cases?

I think going forward I'll just adopt a policy of "whatever the user had and then throw everything at the end".
Comment 32 Paul Webster CLA 2009-12-14 11:03:51 EST
(In reply to comment #31)
> 
> I think going forward I'll just adopt a policy of "whatever the user had and
> then throw everything at the end".

I guess we have a couple of options here.  I would say our guiding principles should be 1) do what makes sense if we can understand the corner case and 2) at least provide predictability.

We would want to support our main usecases under #1, plus if there's a couple corner cases that show up in our upgrade path and cause pain we should definitely handle that gracefully under #1.

The Devil's in the details, however :-)

PW
Comment 33 Remy Suen CLA 2009-12-14 12:39:53 EST
(In reply to comment #31)
> I think going forward I'll just adopt a policy of "whatever the user had and
> then throw everything at the end".

In hindsight, this is an acceptable idea for parts and stacks but a horrible one for top-level menus in a window and perhaps to a lesser extent, the menu items of said menus.
Comment 34 Remy Suen CLA 2009-12-15 09:38:03 EST
(In reply to comment #33)
> (In reply to comment #31)
> > I think going forward I'll just adopt a policy of "whatever the user had and
> > then throw everything at the end".
> 
> In hindsight, this is an acceptable idea for parts and stacks but a horrible
> one for top-level menus in a window and perhaps to a lesser extent, the menu
> items of said menus.

Although, to be fair, I'm not sure it's possible for an end user to add menus randomly. In 3.x you can turn them on and off but those menus are technically already understood by the system and the user is not arbitrarily adding completely _new_ menus. Merging visibility flags is trivial (by comparison), actual 3-way merging, mm, not so much.
Comment 35 Remy Suen CLA 2009-12-22 09:48:08 EST
(In reply to comment #27)
> I've added code to use the e4xmi resource factory. The reconciler has been
> _disabled_ for the time being since our model files all use the xmi file
> extension at the moment.

The reconciler has been reenabled and copy/paste versions of the contacts and photo demo's xmi files has been created with new e4xmi counterparts. Sash weights are being restored properly but not the window's size. This is a consequence of the renderer IIRC, investigating...
Comment 36 Remy Suen CLA 2009-12-22 10:11:24 EST
(In reply to comment #35)
> Sash
> weights are being restored properly but not the window's size. This is a
> consequence of the renderer IIRC, investigating...

Resolved by bug 298411.
Comment 37 Remy Suen CLA 2010-01-07 08:10:58 EST
A RCP developer can set almost everything in the EMF model. What it cannot capture is _context_. Right now the delta calculation only analyzes the EMF changes. Active part information certainly seems like its within the scope of the delta reconciler but it just all sounds kinda fishy to me...
Comment 38 Remy Suen CLA 2010-01-14 09:22:12 EST
There was a shutdown problem that was caused by my usage of "xmi:id" as an attribute name. I've swapped it to be "xmiId" now.

Dave told me about some DOM APIs that would perform the saving (instead of using XSLT) so I'll look into switching that post-M3.
Comment 39 Remy Suen CLA 2010-01-14 13:58:39 EST
The "strategy" has been devised (use ChangeRecorder). We'll use new bugs to address new problems.