umlet icon indicating copy to clipboard operation
umlet copied to clipboard

Add the ability to include shared properties (lines of text) in multiple objects

Open MarqueIV opened this issue 6 years ago • 6 comments

We're editing a diagram that has well over 150 objects on it, 120 of which are classes, 25 are enums and a few other misc items. We like our classes to have a common background/transparency, and all our enums to have something similar.

Well we had a case for documentation reasons we had to change the background color for the classes. That meant we had to manually edit all 120 of the objects just to change the color. Pretty time-consuming.

So, my idea/suggestion is to define a new mechanism to allow you to include properties from one object into another. To do this you need two things:

  1. A way to identify the source to be included
  2. A way to reference that identifier in the target object.

In reverse order, #2 is easy. You can just use the '#include' keyword in your object's definition. e.g.

#include=myClassProperties

and now the properties (or more accurately all the lines of text) from wherever you have defined 'myClassProperties' will simply be expanded inline into the target location as if you had typed them there manually.

Note: Of course you'd need a way to escape the # should you want to display the string '#include' as-is for some reason, and not trigger an actual include, but the same thing needs to happen for all your other keywords.

As for defining what 'myClassProperties' actually resolves to, I was originally thinking that you would simply add a line like the following to any object and that would become a potential source:

#id=myClassProperties

But there are too many down-sides; everything from those 'include objects' appearing on the canvas, collisions in those IDs, not-fully-defined objects (as they would only have the shared properties), and the biggest is with parsing. It would require too much effort to keep track of everything if an #include line was encountered before the definition of that include as the visual order on screen has nothing to do with the order the objects themselves are defined.

So to get around all of those issues, rather than specifying an object to be included, a simple alternative would be to define them in the document's properties, like so...

// Uncomment the following line to change the fontsize and font:
// fontsize=14
fontfamily=Monospaced

#id=myClassProperties
bg=yellow
fg=blue
transparency=50

#id=myEnumProperties
bg=cyan
fg=black
transparency=40

Again, that would get around every one of those earlier limitations.

As for the format, each 'include' would be defined as a line starting with #id= along with the name of the include (e.g. #id= myClassProperties), then starting on the next line, you'd grab everything until you hit one of the following conditions:

  1. A blank line
  2. Another #id line
  3. The end of the file.

That way your definitions are stored right in the document, but aren't displayed on the canvas.

As mentioned, this really simplifies the parsing issue as well. When parsing the document's properties (which you would do before any of its child objects), just like with anything else, it's done in a top-down manner, so, every time you come across a new #id=xxx line, grab the key, then read the following lines using the rules described above, and when you hit the end, take that entire block of text and store it in the lookup using the key from the #id line.

Conversely, when parsing an object (or even further down the document properties), every time you hit an #include=xxx line, you pull the value out of the lookup and insert it inline.

Using that approach also has the benefit of avoiding the following recursive references...

#id=allClassProperties
include=baseClassProperties
bg=yellow
fg=blue
transparency=50

#id=baseClassProperties
#include=allClassProperties
fg=red

This is because when the parser hits the line include=baseClassProperties, it would warn you that you haven't yet defined anything with that ID as it wouldn't yet be in the lookup. Display a simple warning to the user and you're done!

In contrast, the line @#include=allClassProperties would be fine as by the time you got to that line, 'allClassProperties' would be defined as it resides above it.

There are other benefits to this approach too than simple properties. Since you're actually including the text as-is, you can simplify using custom drawing too that would still be compatible with the web version. For instance, in your Custom Drawings example, you have something that renders a DB iconography right within the symbol by adding these lines...

customelement=
// DB Symbol
drawArc(5,5,26,8,0,180,true) fg=black bg=#222222 //Parameters (x, y, width, height, start, extent, open)
drawArc(5,5,26,8,180,180,true) fg=black //Parameters (x, y, width, height, start, extent, open)
drawRectangle(5,9,26,15) lw=0.1 bg=#222222 //Parameters (x, y, width, height)
drawLine(5,9,5,24)fg=black //Parameters (x1, y1, x2, y2) 
drawLine(31,9,31,24)fg=black //Parameters (x1, y1, x2, y2) 
drawArc(5,10,26,8,180,180,true) fg=black //Parameters (x, y, width, height, start, extent, open)
drawArc(5,15,26,8,180,180,true) fg=black //Parameters (x, y, width, height, start, extent, open)
drawArc(5,20,26,8,180,180,true)fg=black bg=#222222 //Parameters (x, y, width, height, start, extent, open)

If you have four such symbols, you've repeated that drawing code four times. Fifty symbol, fifty times, etc.

If you instead however add those lines as an include resource in the document, like this...

#id=dbIcon
customelement=
// DB Symbol
drawArc(5,5,26,8,0,180,true) fg=black bg=#222222 //Parameters (x, y, width, height, start, extent, open)
drawArc(5,5,26,8,180,180,true) fg=black //Parameters (x, y, width, height, start, extent, open)
drawRectangle(5,9,26,15) lw=0.1 bg=#222222 //Parameters (x, y, width, height)
drawLine(5,9,5,24)fg=black //Parameters (x1, y1, x2, y2) 
drawLine(31,9,31,24)fg=black //Parameters (x1, y1, x2, y2) 
drawArc(5,10,26,8,180,180,true) fg=black //Parameters (x, y, width, height, start, extent, open)
drawArc(5,15,26,8,180,180,true) fg=black //Parameters (x, y, width, height, start, extent, open)
drawArc(5,20,26,8,180,180,true)fg=black bg=#222222 //Parameters (x, y, width, height, start, extent, open)

You could now add it to any element by adding a single line of code, like so (the example from the app)...

// Class Element

EntityDAO
--
-id: Long
_-ClassAttribute: Long_
--
#Operation(i: int): int
/+AbstractOperation()/

#include=dbIcon

External Includes

Extending this concept even further, you could reference external documents so you could share all of your colors/styles, etc. as well as all of your custom graphics, such as the aforementioned DB graphic. Maybe you have a library of such graphics that you want to reuse across multiple diagrams.

To do this, I propose the 'fileInclude' keyword, which like the #id keyword, In the document you could add something like this...

#fileInclude=myCommonIncludes.txt

...and that would be expanded inline, just like anything else.

You may even consider auto-including a file if its name pattern-matches the original file. e.g.

  • original file: MainDiagram.uxf
  • Auto-included file: MainDiagram.uxi (for 'uxf-include')

...or something similar. Honestly though, not sure how much value there would be to the auto-included part as to add it manually it's just a single line, but still... just throwing out more ideas.

Anyway, that's what I've got for you. I think it would be an awesome addition and would make what I just had to deal with much easier as I would've only had to change things in one place instead of 120 which made me frustrated enough to take time off and think about how I'd solve this, instead of completing my diagrams, which my boss just wondered where they were. :)

MarqueIV avatar Sep 06 '18 16:09 MarqueIV

I like the idea and it could be helpful, especially with the custom drawings, so I can imagine the basic version (define text blocks in diagram properties and include them in elements) with the potential of future additions (although definitions in external files would break the concept of "everything is within the uxf" which make sharing diagrams easier)

That said, at the moment I don't have the time to implement complex new features and focus more on fixing bugs, but it's a nice idea for future versions.

For the time being: as uxf files are simple xml files, I would recommend using any text editor to search&replace strings instead of using UMLet for mass replacements. It's much faster.

afdia avatar Sep 09 '18 19:09 afdia

While I don't put as much weight as you do into the 'all in one document' approach (and have several suggestions to address that should you be interested) I'd be willing to forego that as a request as I can always copy/paste what I need into the document.

However, I'm not sure how the main portion--including common properties--would be considered 'complex'. I only see two changes needed.

  1. Creating that global lookup of include-IDs paired to the actual to-be-included text which is simple parsing of the document.
  2. Changing the existing item parsing to look for the include line and handle accordingly.

Handling the include line is simple recursion. As you loop through the normal lines of text, if you find an include line, simply start looping through the lines of the included text and processing them as if they were any other line you're processing. That's just a simple recursive function call, exactly the same as if you were parsing the locally-defined lines.

You wouldn't even have to keep track of which nodes referenced which includes as simply invalidating all nodes when you edit the main document--just as you do now when say changing the font family--would address that as well.

Are you envisioning something more complex than the above?

MarqueIV avatar Sep 10 '18 03:09 MarqueIV

To illustrate what I'm talking about off the top of my head, it would be two parts...

The first part is populating the IncludedTextLookup dictionary when reading the document properties. That's described above. Assume the results of that are stored in a key/value store (i.e. a dictionary, etc.) named IncludedTextLookup.

Here's pseudo-code illustrating how the existing parsing would have to change to accommodate it...

From this...

void ProcessLines(){

    string localText = [wherever this comes from];
    foreach(var line in localText.ToLines()){
        // process lines as usual
    }
}

To this...

void ProcessLines(){

    string localText = [wherever this comes from];
    ProcessLinesWorker(localText);    
}

void ProcessLinesWorker(string propertyText){

    foreach(var line in propertyText.ToLines()){

        if(line.startsWith("#include")) {  // We've found an include line
            string id = extractIdFromLine(line); // Get the ID
            string includedText = IncludedTextLookup[id]; // Get the included text from the lookup**
            ProcessLinesWorker(includedText); // Recurse back in with that included text
            continue; // We're done so continue onto the next line
        }

        // process lines as usual
    }
}

**If the ID isn't found in the lookup, just render a text line on the object on the canvas saying 'Missing ID: xxx' and just continue on to the next line.

I'm not a Java expert (I'm Swift and C# but I do have some Android) but I can't imagine this would take more than an hour or so to implement. Again, it's just a key/value store parsed from the document properties, then simple recursion in the individual, already-existing object-line parser.

C'mon! Take it as a coding-challenge to see if I'm right! I really bet it'll take a lot less effort than you're thinking, and the up-side is huge, and sure beats having to use an external tool to find/replace values in XML! :)

MarqueIV avatar Sep 10 '18 04:09 MarqueIV

Just wondering if you've given this any more thought. Tried to figure out how to build the code so I could take a stab at it myself, but I'm just not familiar enough with building java apps outside of Android anyway. Still, I have to think this isn't that complicated/complex based on the above design.

MarqueIV avatar Sep 17 '18 22:09 MarqueIV

Sorry, the term "complex feature" was wrong. It's not so much about the complexity of the feature, I just meant that I'm currently maintaining Umlet in terms of fixing bugs, but I do not have the time to implement new features. Umlet is mostly developed by students who collect work experience by adding new features and I guess this would be a nice example for the next student, because it's not that hard to implement but quite useful. Therefore with Umlet you will often see times with low activity and times with much higher activity.

About the feature: I would implement it as:

  • the diagram panel may contain text blocks which can be referenced using an id
  • the text blocks are stored in the diagram handler
  • elements check this store when they are drawn to manipulate the properties text before the parsing begins (a simple searc&replace on the properties before the real parsing procedure starts)

The main open question is: what is the best way to declare the id and the boundary of the text block. e.g. I'm not sure if an empty line is the best way to "end" the block because perhaps someone wants an empty line as part of the block, maybe it would be better to use { ... }

first line which is not part of the block
replacementId {
first line of the block

last line of the block
}
other line which is not part of the block

But as mentioned before I don't know when it will be implemented, but I really like the idea and it's simple (compared to other feature requests), so it's probably high on our priority list

afdia avatar Sep 18 '18 05:09 afdia

I think using the braces would be fine. Even simpler, you could just say 'Include everything until either the next ID line or the end of the file' but that would require you to put them at the bottom of the document. The braces would let you put it anywhere. Then again, maybe a separate tab in the UI for these? It would still be maintained in the document, but wouldn't me 'mixed in' with the document's own properties. Just throwing that out as an option. Not married to anything in particular.

As far as the search and replace, make sure that whatever design you do, that it's recursive in nature (i.e. one ID can reference another one so long as the other one was defined before/above it. That lets you 'cascade' properties to multiple things. Think CSS.

Additionally items on the canvas can reference multiple IDs to 'compose' their UIs from common elements.)

Anyway, I still owe you an Eclipse-on-Mac test. Just been so busy doing other things it keeps slipping my mind, but I do want to make sure I'm doing my part to support/help you guys since the work you're doing helps me do mine.

Thanks again, and let me know if there's any way I can help with the above.

MarqueIV avatar Sep 18 '18 05:09 MarqueIV