View Issue Details

IDProjectCategoryView StatusLast Update
0035125LazarusTAChartpublic2019-03-02 01:27
Reporterwp Assigned Towp  
PrioritynormalSeverityminorReproducibilityalways
Status closedResolutionfixed 
Product Version2.2 
Summary0035125: Streaming error of TListChartSource property DataPoints
DescriptionThe property DataPoints of TListChartSource can be used to easily create data for demo charts.

In r60425, the associated property editor has been extended to support multiple x values per data point.

While the extraction of chart data from the the DataPoints string list works correctly, a deviation can be observed after reading from lfm file.
Steps To Reproduce- Create a new project
- Add a TListChartSource (chart not needed)
- Set both XCount and YCount to 2
- Click on the '...' next to "Datapoints" to open the property editor
- In the first row of the grid enter the values 1, 2, 3, 4 to the columns X1, X2, Y1 and Y2; columns Color and Text can be empty
- In the second row enter: 10, 20, 30, 40
- OK --> the object inspector displays the string
     '1|2|3|4|?| 10|20|30|40|?|'
  (the space before the "10" is a non-dispaying line break)
- Save
- Open the lfm file in an external edit: The Datapoints.Strings is correct:
    DataPoints.Strings = (
      '1|2|3|4|?|'
      '10|20|30|40|?|'
    )
- Load the project again into Lazarus
- Select the TListChartSource and look at the Datapoints property: Now it displays the string
    '1|0|2|3|?| 10|0|20|30|?|'

Note the missing 4 and 40 as well as the inserted 0 between 1 and 2 as well as between 10 and 20.
Additional InformationDebugging during the loading of the form shows that XCount and YCount are not 2, but 1, at the time when the strings are analyzed.
TagsNo tags attached.
Fixed in Revision
LazTarget-
Widgetset
Attached Files

Relationships

related to 0035155 closedwp TAChart: trivial streaming issue in TListChartSourceStrings 

Activities

Marcin Wiazowski

2019-02-21 03:24

reporter   ~0114318

I discovered some issues when working on the problem with LFM loading, so I'm sending some preliminary fixes in patch1.diff:



1) I solved a mystery of the statement:

  if (FSource.XCount = 1) and (FSource.YCount + 3 < Cardinal(parts.Count)) then
    FSource.YCount := parts.Count - 3;

I updated the code as needed, and added a comment: "There should be XCount + YCount + 2 (for Color and Text) parts of the string - but if there are more parts, we increase YCount accordingly"



2) I discovered, that - when a "if FSource.YCount > 0 then" condition is not met - Y variable becomes uninitialized, in particular it might contain the previous value; so I initialized it to NaN in this case.



3) Optimization:

  function TListChartSource.NewItem: PChartDataItem;
  begin
    New(Result); <==== guarantees that Result^.XList and Result^.YList are empty
    SetLength(Result^.XList, Max(XCount - 1, 0));
    SetLength(Result^.YList, Max(YCount - 1, 0));
  end;

For almost all data sources, XCount and YCount will be 1, so calling SetLength is useless. Compiler guarantees, that XList and YList are null pointers initially (i.e. lists are empty) - otherwise "SetLength" calls would operate on random pointers, which would cause crashes. So there is no need of calling "SetLength" for XCount / YCount < 2.



4) Optimization in function CompareDataItemX(): by using "absolute" statements, we can avoid copying data between parameters and local variables.

Marcin Wiazowski

2019-02-21 03:24

reporter  

patch1.diff (2,211 bytes)   
Index: components/tachart/tasources.pas
===================================================================
--- components/tachart/tasources.pas	(revision 60471)
+++ components/tachart/tasources.pas	(working copy)
@@ -352,8 +352,11 @@
 begin
   parts := Split(AString);
   try
-    if (FSource.XCount = 1) and (FSource.YCount + 3 < Cardinal(parts.Count)) then
-      FSource.YCount := parts.Count - 3;
+    // There should be XCount + YCount + 2 (for Color and Text) parts of the
+    // string - but if there are more parts, we increase YCount accordingly
+    if FSource.XCount + FSource.YCount + 2 < Cardinal(parts.Count) then
+      FSource.YCount := Cardinal(parts.Count) - FSource.XCount - 2;
+
     with ADataItem^ do begin
       X := StrToFloatOrDateTimeDef(NextPart);
       if FSource.XCount > 1 then
@@ -363,7 +366,8 @@
         Y := StrToFloatOrDateTimeDef(NextPart);
         for i := 0 to High(YList) do
           YList[i] := StrToFloatOrDateTimeDef(NextPart);
-      end;
+      end else
+        Y := NaN;
       Color := StrToIntDef(NextPart, clTAColor);
       Text := NextPart;
     end;
@@ -550,8 +554,10 @@
 function TListChartSource.NewItem: PChartDataItem;
 begin
   New(Result);
-  SetLength(Result^.XList, Max(XCount - 1, 0));
-  SetLength(Result^.YList, Max(YCount - 1, 0));
+  if XCount > 1 then
+    SetLength(Result^.XList, XCount - 1);
+  if YCount > 1 then
+    SetLength(Result^.YList, YCount - 1);
 end;
 
 procedure TListChartSource.SetColor(AIndex: Integer; AColor: TChartColor);
@@ -751,11 +757,9 @@
 function CompareDataItemX(AItem1, AItem2: Pointer): Integer;
 var
   i: Integer;
-  item1, item2: PChartDataItem;
+  item1: PChartDataItem absolute AItem1;
+  item2: PChartDataItem absolute AItem2;
 begin
-  item1 := PChartDataItem(AItem1);
-  item2 := PChartDataItem(AItem2);
-
   Result := CompareFloat(item1^.X, item2^.X);
   if Result = 0 then
     for i := 0 to Min(High(item1^.XList), High(item2^.XList)) do begin
@@ -1424,7 +1428,7 @@
 
   FOriginYCount := FOrigin.YCount;
   if ReorderYList = '' then begin
-    SetLength(FYOrder,  FOriginYCount);
+    SetLength(FYOrder, FOriginYCount);
     for i := 0 to High(FYOrder) do
       FYOrder[i] := i;
   end
patch1.diff (2,211 bytes)   

wp

2019-02-21 17:11

developer   ~0114332

Do I understand correctly that (1) does not yet solve the streaming issue?

I would expect that because my experiments showed that when the string parser gets involved the FSource.XCount and FSource.YCount are still 1, their true value will be read from the lfm later. Before multiple x values were allowed this "mysterious" line helped to calculate the true YCount from the number of items in the string line because XCount was always 1. But now there is a condition XCount + YCount + 2 = (number of items), and without knowing one of the two it is not possible to extract BOTH XCount and YCount from this single condition. Therefore, this line is useless now and must be removed to ignore items in extra long lines, or an exception must be raised because the data structure does not match the assumptions.

I'll ask in the devs mailing list how the order of properties can be changed in the lfm.

Marcin Wiazowski

2019-02-21 18:07

reporter   ~0114333

You don't have to ask on the mailing list - I already have working code, but I must test it.

(1) is a needed part of the patch - it's just a preparation for cases, when XCount is not 1.

Marcin Wiazowski

2019-02-21 18:09

reporter   ~0114334

I'll attach here next patches when ready. Applying all of them will finally solve the problem. I'm just solving the problem in stages.

Marcin Wiazowski

2019-02-21 18:15

reporter   ~0114335

> Therefore, this line is useless now and must be removed to ignore items in extra long lines

I initially removed this line, but it broke compatibility - The "tachart\test\test.lpr" test application started to fail.

So I adjusted the code (in patch1.diff) to be both compatible and working also with XCount <> 1.

But if you prefer to raise an exception (this will break the compatibility), I won't mind.

wp

2019-02-21 18:33

developer   ~0114336

Last edited: 2019-02-21 18:36

View 3 revisions

Already got an answer from Mattias: "Do not rely on property order! And do not execute properties immediately, e.g. descendant forms may change the properties. Wait with applying properties until Loaded is called." Your solution also?

Marcin Wiazowski

2019-02-21 18:39

reporter   ~0114337

I just done it in the way, that Mattias points: just by caching point data and applying it in the "Loaded" virtual method.

Marcin Wiazowski

2019-02-22 01:20

reporter   ~0114344

It would be helpful for me to know, if I should expect adjusting FSource.YCount in TListChartSourceStrings.Parse (as it is now and in the attached patch), or rather raising an exception there in case, when FSource.XCount + FSource.YCount + 2 is different than Cardinal(parts.Count).

If you could decide and either apply the patch1.diff as it is, or with your preferred modifications, I could continue testing.

wp

2019-02-22 09:25

developer   ~0114346

I don't know yet whether an exception will be raised or the faulty line(s) will just be indicated by a message box. But the Parse method should not change the YCount as it is now. Just focussing on YCount is not correct, because the user might have wanted also to make the XCount grow in this case. In order to prioritize, of course, the 1st y value could be marked by a prefix, e.g. 'y:' (--> '1|2|y:3|4|?|''), but the problem is now what happens when there are less items than needed? Did the user want a NaN in the missing positions, or was the item just forgotten? Consideration of such cases is avoided when a change of XCount and YCount is not allowed in the Parse method.

Marcin Wiazowski

2019-02-22 11:40

reporter   ~0114347

Now I'm also even more convinced of Parse should not change YCount.

Under normal circumstances, Parse is only used when loading from stream. Because:
- data point editor in IDE guarantees, that the stream contains valid strings,
- thanks to patch, we will know valid XCount and YCount values during a Parse call,

I think that raising an exception is a good solution - needed just for those cases, when someone edited LFM manually. Since exception will lead to message box, all your and my wishes will be satisfied.

So I will:
- try to load all strings having valid length,
- try to ignore all strings having invalid length,
- and finally raise an exception if any string had invalid length.

And I'll be back with a final patch later.

Marcin Wiazowski

2019-02-23 04:25

reporter   ~0114357

Important 1: I'm sending you a test.diff - it is ONLY for testing/validation purposes, do NOT apply it to the repository - this would make loading TListChartSource from LFM impossible.

Important 2: The attached test.diff is still NOT a solution for the problem with loading from LFM - but it is a required part of it.



So why I'm sending test.diff? It cannot be used to load TListChartSource from LFM, but it can be used to test TListChartSource created dynamically, during the runtime.

The patch implements two functionalities:
- if string being loaded is not coherent with current XCount and YCount values, an exception is raised,
- if multiple strings are loaded at once (one string per each data point), valid strings are applied, invalid strings are ignored, an an exception is raised at the end of loading process - not immediately, this would disable loading further strings (possibly valid).



Let's assume that we have XCount = 2 and YCount = 2. I analyzed some available code and realized, that valid string contents, in particular, should be:

- 1|2|3|4
- 1|2|3|4|$123456
- 1|2|3|4|$123456|abcd

So validation algorithm is:

  if (Cardinal(parts.Count) < FSource.XCount + FSource.YCount) or
     (Cardinal(parts.Count) > FSource.XCount + FSource.YCount + 2) then
    ERROR
  else
    OK;



Introduced changes are:

A) Newly introduced variable TListChartSourceStrings.FAddingMultiple tells if we are in the multiple-string loading mode (so exceptions should be suppressed for the moment). It may seem that this could be just a Boolean variable, but Integer has used instead, to allow support for nested calls (which increment/decrement the variable)

B) Newly introduced variable TListChartSourceStrings.FAddingMultipleFailureCount tells if there were some invalid strings

C) The user may refer to DataPoints in many ways, in particular:
    - TListChartSource.DataPoints.Add('...')
    - TListChartSource.DataPoints.Insert('...')
    - TListChartSource.DataPoints.AddStrings() - multiple strings can be added at once
    - TListChartSource.DataPoints.AddText('...') - multiple strings can be added at once
    - TListChartSource.DataPoints.Text := '...' - multiple strings can be added at once
    - TListChartSource.DataPoints.Assign() - multiple strings can be added at once
    - TListChartSource.DataPoints.LoadFromStream() - multiple strings can be added at once

   To handle multiple-string loading, the following virtual methods must have been overriden:
    - TListChartSourceStrings.SetTextStr
    - TListChartSourceStrings.AddStrings
    - TListChartSourceStrings.AddText
   and also their low-level helpers:
    - TListChartSourceStrings.Add
    - TListChartSourceStrings.AddObject

D) The private TListChartSourceStrings.Parse method have been changed from procedure to function - so, in case of invalid string, it either raises an exception immediately in single-string loading mode, or just returns False in multiple-string loading mode

E) The TListChartSourceStrings.Insert procedure has been modified to prevent memory leaks in case of exception



I'm attaching also a Test application. Is raises some exceptions when running under debugger - please just continue execution, all of them should be suppressed - i.e. no exception will be reported by the application.



So I'd like to ask you for some validation: please apply the patch locally, look at changes that I made, experiment with Test application and tell me if you accept these changes. If so, they will become an integral part of the final patch.

Marcin Wiazowski

2019-02-23 04:25

reporter  

test.diff (5,969 bytes)   
Index: components/tachart/tachartstrconsts.pas
===================================================================
--- components/tachart/tachartstrconsts.pas	(revision 60471)
+++ components/tachart/tachartstrconsts.pas	(working copy)
@@ -71,6 +71,8 @@
   // Chart sources
   rsSourceCountError = '%0:s requires a chart source with at least %1:d %2:s value(s) per data point.';
   rsSourceCountError2 = 'This %0:s instance must have at least %1:d %2:s value(s) per data point.';
+  rsSourceStringFormatError = 'Passed string cannot be converted to data point.';
+  rsSourceStringFormatError2 = 'One or more passed string(s) cannot be converted to data point(s).';
 
   // Transformations
   tasAxisTransformsEditorTitle = 'Edit axis transformations';
Index: components/tachart/tacustomsource.pas
===================================================================
--- components/tachart/tacustomsource.pas	(revision 60471)
+++ components/tachart/tacustomsource.pas	(working copy)
@@ -73,6 +73,7 @@
   EEditableSourceRequired = class(EChartError);
   EXCountError = class(EChartError);
   EYCountError = class(EChartError);
+  EStringFormatError = class(EChartError);
 
   TChartValueText = record
     FText: String;
Index: components/tachart/tasources.pas
===================================================================
--- components/tachart/tasources.pas	(revision 60471)
+++ components/tachart/tasources.pas	(working copy)
@@ -258,17 +258,25 @@
   TListChartSourceStrings = class(TStrings)
   strict private
     FSource: TListChartSource;
-    procedure Parse(AString: String; ADataItem: PChartDataItem);
+    FAddingMultiple: Integer;
+    FAddingMultipleFailureCount: Integer;
+    function Parse(AString: String; ADataItem: PChartDataItem): Boolean;
   protected
     function Get(Index: Integer): String; override;
     function GetCount: Integer; override;
     procedure Put(Index: Integer; const S: String); override;
     procedure SetUpdateState(AUpdating: Boolean); override;
+    procedure SetTextStr(const Value: String); override;
   public
     constructor Create(ASource: TListChartSource);
     procedure Clear; override;
     procedure Delete(Index: Integer); override;
     procedure Insert(Index: Integer; const S: String); override;
+    function Add(const S: string): Integer; override;
+    function AddObject(const S: string; AObject: TObject): Integer; override;
+    procedure AddStrings(TheStrings: TStrings); override;
+    procedure AddStrings(const TheStrings: array of String); override;
+    procedure AddText(Const S : String); override;
   end;
 
 procedure Register;
@@ -327,13 +335,21 @@
   item: PChartDataItem;
 begin
   item := FSource.NewItem;
-  FSource.FData.Insert(Index, item);
-  Parse(S, item);
+  try
+    if not Parse(S, item) then begin
+      Dispose(item);
+      exit;
+    end;
+    FSource.FData.Insert(Index, item);
+  except
+    Dispose(item);
+    raise;
+  end;
   FSource.UpdateCachesAfterAdd(item^.X, item^.Y);
 end;
 
-procedure TListChartSourceStrings.Parse(
-  AString: String; ADataItem: PChartDataItem);
+function TListChartSourceStrings.Parse(
+  AString: String; ADataItem: PChartDataItem): Boolean;
 var
   p: Integer = 0;
   parts: TStrings;
@@ -350,10 +366,20 @@
 var
   i: Integer;
 begin
+  Result := true;
+
   parts := Split(AString);
   try
-    if (FSource.XCount = 1) and (FSource.YCount + 3 < Cardinal(parts.Count)) then
-      FSource.YCount := parts.Count - 3;
+    // There should be XCount + YCount .. XCount + YCount + 2 (for Color and Text)
+    // parts of the string
+    if (Cardinal(parts.Count) < FSource.XCount + FSource.YCount) or
+       (Cardinal(parts.Count) > FSource.XCount + FSource.YCount + 2) then
+    if FAddingMultiple <> 0 then begin
+      Inc(FAddingMultipleFailureCount);
+      exit(false);
+    end else
+      raise EStringFormatError.Create(rsSourceStringFormatError);
+
     with ADataItem^ do begin
       X := StrToFloatOrDateTimeDef(NextPart);
       if FSource.XCount > 1 then
@@ -390,6 +416,81 @@
     FSource.EndUpdate;
 end;
 
+function TListChartSourceStrings.Add(const S: string): Integer;
+begin
+  Result := Count;
+  Insert(Result, S);
+  if Result = Count then
+    Result := -1;
+end;
+
+function TListChartSourceStrings.AddObject(const S: string; AObject: TObject): Integer;
+begin
+  Result := Add(S);
+  if Result >= 0 then
+    Objects[Result] := AObject;
+end;
+
+procedure TListChartSourceStrings.SetTextStr(const Value: String);
+begin
+  Inc(FAddingMultiple);
+  try
+    inherited;
+  finally
+    Dec(FAddingMultiple);
+  end;
+  if (FAddingMultiple = 0) and (FAddingMultipleFailureCount <> 0) then
+  begin
+    FAddingMultipleFailureCount := 0;
+    raise EStringFormatError.Create(rsSourceStringFormatError2);
+  end;
+end;
+
+procedure TListChartSourceStrings.AddStrings(TheStrings: TStrings);
+begin
+  Inc(FAddingMultiple);
+  try
+    inherited;
+  finally
+    Dec(FAddingMultiple);
+  end;
+  if (FAddingMultiple = 0) and (FAddingMultipleFailureCount <> 0) then
+  begin
+    FAddingMultipleFailureCount := 0;
+    raise EStringFormatError.Create(rsSourceStringFormatError2);
+  end;
+end;
+
+procedure TListChartSourceStrings.AddStrings(const TheStrings: array of String);
+begin
+  Inc(FAddingMultiple);
+  try
+    inherited;
+  finally
+    Dec(FAddingMultiple);
+  end;
+  if (FAddingMultiple = 0) and (FAddingMultipleFailureCount <> 0) then
+  begin
+    FAddingMultipleFailureCount := 0;
+    raise EStringFormatError.Create(rsSourceStringFormatError2);
+  end;
+end;
+
+procedure TListChartSourceStrings.AddText(Const S : String);
+begin
+  Inc(FAddingMultiple);
+  try
+    inherited;
+  finally
+    Dec(FAddingMultiple);
+  end;
+  if (FAddingMultiple = 0) and (FAddingMultipleFailureCount <> 0) then
+  begin
+    FAddingMultipleFailureCount := 0;
+    raise EStringFormatError.Create(rsSourceStringFormatError2);
+  end;
+end;
+
 { TListChartSource }
 
 function TListChartSource.Add(
test.diff (5,969 bytes)   

Marcin Wiazowski

2019-02-23 04:25

reporter  

Test.zip (3,719 bytes)

wp

2019-02-23 11:51

developer   ~0114361

You give a thorough implementation of all Add* methods of TListChartSourceStrings inherited by TStrings, and this gives me the impression that you consider TListChartSource.DataPoints as being a generally accepted way to enter data to the list source.

It is not. Although documentation often is out-dated I fully agree with the sentence in TADocumentation saying "Note that DataPoints property is designed primarily for sample and demo code. It is very inefficient, and you should not use it to add data points from the code."

From this point of view all the Add* methods are not needed, their explicit presence in TAChart code now (before your code they were "buried" in classes) even motivates users to apply them instead of the favored Add* methods of the TListChartSource itself.

Your patch should only solve the streaming issue. Sorry to disappoint you, but I don't want to add code for a usage scenario which is not encouraged.

In the long term, TListChartSourceStrings should be made to inherit from TPersistent instead of TStrings to completely remove the Add* methods at all.

Marcin Wiazowski

2019-02-23 16:47

reporter   ~0114366

> In the long term, TListChartSourceStrings should be made to inherit from TPersistent instead of TStrings to completely remove the Add* methods at all.

Please take a look the the TListChartSource declaration:

  TListChartSource = class(TCustomChartSource)
  ...
  published
    property DataPoints: TStrings read FDataPoints write SetDataPoints;
  end;

DataPoints is TStrings - and TListChartSourceStrings is just an internal implementation of this TStrings. So, in the long term, TListChartSourceStrings can NOT become inheriting from TPersistent instead of TStrings, without introducing a major incompatibility in the TListChartSource itself.



Although "DataPoints property is designed primarily for sample and demo code. It is very inefficient, and you should not use it to add data points from the code." - it is exactly what is done in LFM loader - it initializes TListChartSource by operating on its TListChartSource.DataPoints.



Maybe I was trying to make the patch too good - i.e. I was trying to load all the valid strings, and raise an exception *later*. Multiple strings are loaded in particular when calling TListChartSource.DataPoints.LoadFromStream() and TListChartSource.DataPoints.Assign().

You can decide to fail immediately, at the first string. In this case:
- FAddingMultiple and FAddingMultipleFailureCount variables can be removed,
- SetTextStr(), AddStrings() and AddText() overriding methods can be removed - please note that they are just simple wrappers for handling FAddingMultiple and FAddingMultipleFailureCount. They are called internally when you call TListChartSource.DataPoints.LoadFromStream() or TListChartSource.DataPoints.Assign(),
- Add() and AddObject() overriding methods can be removed - they are needed for cases, when Parse() returns False, so Add() and AddObject() must return -1 - Add() and AddObject() are the lowest-level handlers when loading from stream, so they must return valid result. Since Parse () will be reverted to a procedure, Add() and AddObject() will never need to return -1, so they may be removed.



So I think that you have three possibilities:
- change TListChartSource.DataPoints from TStrings to something else - then you are completely on your own,
- accept changes from test.diff as they are,
- decide to raise an exception immediately, at the first invalid string.



What is your decision?

wp

2019-02-23 19:01

developer   ~0114372

>> In the long term, TListChartSourceStrings should be made to inherit from TPersistent instead
>> of TStrings to completely remove the Add* methods at all.
> in the long term, TListChartSourceStrings can NOT become inheriting from TPersistent instead
> of TStrings, without introducing a major incompatibility in the TListChartSource itself.

You are right. In principle. But I would not call it a *major* incompatibility. Almost nobody (except for youself) is adding data via DataPoints property. Since in this case DataPoints does not show up in the lfm file, almost nobody will be affected.

----

> Although "DataPoints property is designed primarily for sample and demo code. It is very inefficient, and
> you should not use it to add data points from the code." - it is exactly what is done in LFM loader - it
> initializes TListChartSource by operating on its TListChartSource.DataPoints.

If you want the possibility to add data at design time I cannot imagine any other way than storing numeric values as strings in the lfm file. And this is always less efficient than having the values immediately as floats. Therefore it is not recommended as the "normal" way to add data.

----

> [...] are called internally when you call TListChartSource.DataPoints.LoadFromStream() or TListChartSource.DataPoints.Assign().

As I said TListChartSource.DataPoints should not appear in user code. AFAIK, DataPoints.LoadFromStream and .Assign are not called anywhere. An indirect Assign via ListChartSource.Assign cannot occur because Assign is not implemented for the chart sources. Well, ok, somebody could abuse DataPoints.LoadFromStream to load data from file or so... I've been thinking about providing an interface to file access lately...

----

> So I think that you have three possibilities:
> - change TListChartSource.DataPoints from TStrings to something else - then you are completely on your own,
> - accept changes from test.diff as they are,
> - decide to raise an exception immediately, at the first invalid string.
>
> What is your decision?

Raise an exception immediately, I don't see a reason why to continue parsing - the file structure is not ok, and we do not know what else is wrong. It's not worth to put too much effort into this corner case. I am aware that inout is lost this way. But the user cannot enter this state via property editor dialog; the issue can only happen when the lfm file is edited (see your comments in another discussion) or when the concatenated numbers are typed into DataPoints edit field directly - if anybody should try this he will never do it again...

Marcin Wiazowski

2019-02-23 21:46

reporter   ~0114376

> Almost nobody (except for youself) is adding data via DataPoints property.

I appreciate your joke. As we both already know, I'm a fan of efficiency and fast execution. I would never have thought about adding any data by using DataPoints property - but this is what I found in the "tachart\test\test.lpr" test application. So I just was trying to be compatible with that.



I'm attaching a final patch.

General idea is: when LFM loader starts its work, it sets a "csLoading" flag in the component's "ComponentState" property. When LFM loader finishes its work, it calls the component's "Loaded" virtual method. During the loading process, TListChartSourceStrings accumulates coming data in its internal FLoadingCache. When "Loaded" is called, the accumulated data is finally applied.

Since we want to load XCount from LFM together with YCount, we should handle XCount in the same way as YCount when streaming - i.e. in TListChartSourceStrings's Parse() and Get() methods. After applying the patch, Parse() and Get() methods provide this.

After applying a patch and rebuilding Lazarus IDE, loading a form in IDE, that has invalid DataPoints contents, just displays an error message and opens LFM file in the editor.

Marcin Wiazowski

2019-02-23 21:47

reporter  

final.diff (6,759 bytes)   
Index: components/tachart/tachartstrconsts.pas
===================================================================
--- components/tachart/tachartstrconsts.pas	(revision 60475)
+++ components/tachart/tachartstrconsts.pas	(working copy)
@@ -71,6 +71,7 @@
   // Chart sources
   rsSourceCountError = '%0:s requires a chart source with at least %1:d %2:s value(s) per data point.';
   rsSourceCountError2 = 'This %0:s instance must have at least %1:d %2:s value(s) per data point.';
+  rsSourceStringFormatError = 'Passed string cannot be converted to data point.';
 
   // Transformations
   tasAxisTransformsEditorTitle = 'Edit axis transformations';
Index: components/tachart/tacustomsource.pas
===================================================================
--- components/tachart/tacustomsource.pas	(revision 60475)
+++ components/tachart/tacustomsource.pas	(working copy)
@@ -73,6 +73,7 @@
   EEditableSourceRequired = class(EChartError);
   EXCountError = class(EChartError);
   EYCountError = class(EChartError);
+  EStringFormatError = class(EChartError);
 
   TChartValueText = record
     FText: String;
Index: components/tachart/tasources.pas
===================================================================
--- components/tachart/tasources.pas	(revision 60475)
+++ components/tachart/tasources.pas	(working copy)
@@ -38,6 +38,7 @@
   protected
     function GetCount: Integer; override;
     function GetItem(AIndex: Integer): PChartDataItem; override;
+    procedure Loaded; override;
     procedure SetXCount(AValue: Cardinal); override;
     procedure SetYCount(AValue: Cardinal); override;
   public
@@ -258,7 +259,10 @@
   TListChartSourceStrings = class(TStrings)
   strict private
     FSource: TListChartSource;
+    FLoadingCache: TStringList;
     procedure Parse(AString: String; ADataItem: PChartDataItem);
+  private
+    procedure LoadingFinished;
   protected
     function Get(Index: Integer): String; override;
     function GetCount: Integer; override;
@@ -266,6 +270,7 @@
     procedure SetUpdateState(AUpdating: Boolean); override;
   public
     constructor Create(ASource: TListChartSource);
+    destructor Destroy; override;
     procedure Clear; override;
     procedure Delete(Index: Integer); override;
     procedure Insert(Index: Integer; const S: String); override;
@@ -284,7 +289,10 @@
 
 procedure TListChartSourceStrings.Clear;
 begin
-  FSource.Clear;
+  if not (csLoading in FSource.ComponentState) then
+    FSource.Clear
+  else
+    FreeAndNil(FLoadingCache);
 end;
 
 constructor TListChartSourceStrings.Create(ASource: TListChartSource);
@@ -293,6 +301,12 @@
   FSource := ASource;
 end;
 
+destructor TListChartSourceStrings.Destroy;
+begin
+  inherited;
+  FLoadingCache.Free;
+end;
+
 procedure TListChartSourceStrings.Delete(Index: Integer);
 begin
   FSource.Delete(Index);
@@ -306,20 +320,28 @@
   fs := DefaultFormatSettings;
   fs.DecimalSeparator := '.';
   with FSource[Index]^ do begin
-    Result := Format('%g', [X], fs);
+    Result := '';
+    if FSource.XCount > 0 then
+      Result += Format('%g|', [X], fs);
     for i := 0 to High(XList) do
-      Result += Format('|%g', [XList[i]], fs);
+      Result += Format('%g|', [XList[i]], fs);
     if FSource.YCount > 0 then
-      Result += Format('|%g', [Y], fs);
+      Result += Format('%g|', [Y], fs);
     for i := 0 to High(YList) do
-      Result += Format('|%g', [YList[i]], fs);
-    Result += Format('|%s|%s', [IntToColorHex(Color), Text]);
+      Result += Format('%g|', [YList[i]], fs);
+    Result += Format('%s|%s', [IntToColorHex(Color), Text]);
   end;
 end;
 
 function TListChartSourceStrings.GetCount: Integer;
 begin
-  Result := FSource.Count;
+  if not (csLoading in FSource.ComponentState) then
+    Result := FSource.Count
+  else
+  if Assigned(FLoadingCache) then
+    Result := FLoadingCache.Count
+  else
+    Result := 0;
 end;
 
 procedure TListChartSourceStrings.Insert(Index: Integer; const S: String);
@@ -326,9 +348,21 @@
 var
   item: PChartDataItem;
 begin
+  if csLoading in FSource.ComponentState then begin
+    if not Assigned(FLoadingCache) then
+      FLoadingCache := TStringList.Create;
+    FLoadingCache.Insert(Index, S);
+    exit;
+  end;
+
   item := FSource.NewItem;
-  FSource.FData.Insert(Index, item);
-  Parse(S, item);
+  try
+    Parse(S, item);
+    FSource.FData.Insert(Index, item);
+  except
+    Dispose(item);
+    raise;
+  end;
   FSource.UpdateCachesAfterAdd(item^.X, item^.Y);
 end;
 
@@ -350,20 +384,30 @@
 var
   i: Integer;
 begin
+  // Note: this method is called only when component loading is fully finished -
+  // so FSource.XCount and FSource.YCount are already properly estabilished
+
   parts := Split(AString);
   try
-    if (FSource.XCount = 1) and (FSource.YCount + 3 < Cardinal(parts.Count)) then
-      FSource.YCount := parts.Count - 3;
+    // There should be XCount + YCount .. XCount + YCount + 2 (for Color and Text)
+    // parts of the string
+    if (Cardinal(parts.Count) < FSource.XCount + FSource.YCount) or
+       (Cardinal(parts.Count) > FSource.XCount + FSource.YCount + 2) then
+      raise EStringFormatError.Create(rsSourceStringFormatError);
+
     with ADataItem^ do begin
-      X := StrToFloatOrDateTimeDef(NextPart);
-      if FSource.XCount > 1 then
+      if FSource.XCount > 0 then begin
+        X := StrToFloatOrDateTimeDef(NextPart);
         for i := 0 to High(XList) do
           XList[i] := StrToFloatOrDateTimeDef(NextPart);
+      end else
+        X := NaN;
       if FSource.YCount > 0 then begin
         Y := StrToFloatOrDateTimeDef(NextPart);
         for i := 0 to High(YList) do
           YList[i] := StrToFloatOrDateTimeDef(NextPart);
-      end;
+      end else
+        Y := NaN;
       Color := StrToIntDef(NextPart, clTAColor);
       Text := NextPart;
     end;
@@ -384,12 +428,24 @@
 
 procedure TListChartSourceStrings.SetUpdateState(AUpdating: Boolean);
 begin
-  if AUpdating then
-    FSource.BeginUpdate
-  else
-    FSource.EndUpdate;
+  if not (csLoading in FSource.ComponentState) then
+    if AUpdating then
+      FSource.BeginUpdate
+    else
+      FSource.EndUpdate;
 end;
 
+procedure TListChartSourceStrings.LoadingFinished;
+begin
+  // csLoading in FSource.ComponentState is already cleared
+  if Assigned(FLoadingCache) then
+    try
+      Assign(FLoadingCache);
+    finally  
+      FreeAndNil(FLoadingCache);
+    end;
+end;
+
 { TListChartSource }
 
 function TListChartSource.Add(
@@ -781,6 +837,12 @@
   Notify;
 end;
 
+procedure TListChartSource.Loaded;
+begin
+  inherited; // clears csLoading in ComponentState
+  (FDataPoints as TListChartSourceStrings).LoadingFinished;
+end;
+
 { TMWCRandomGenerator }
 
 function TMWCRandomGenerator.Get: LongWord;
final.diff (6,759 bytes)   

wp

2019-02-23 23:59

developer   ~0114377

Thank you. Like I would have done it myself (except for the trick with the Assign).

Two points:

- The check

    if (Cardinal(parts.Count) < FSource.XCount + FSource.YCount) or
       (Cardinal(parts.Count) > FSource.XCount + FSource.YCount + 2) then (raise)
  
makes the color and text fields optional. When the strings were created by the property editor, fields for color and text are absolutely included. If they are missing something has been manipulated. (BTW, I was wrong by stating that the strings can be edited directly in the OI). I'd prefer to raise the exception in this case, too, i.e. check for

    if (Cardinal(parts.Count) <> FSource.XCount + FSource.YCount + 2) then (raise)

- The message "Passed string cannot be converted to data point" is not very helpful to find the buggy string. I'd prefer to have the faulty string mentioned in the message, i.e. "Passed string "%s" cannot be ..." where %s is to be replaced by the faulty string (shortened to something like 20 characters if it should turn out to be too long). Or: Since the only reason we have for this error is a mismatch between XCount/YCount and the number of string items, the message could also be "Item count in the passed string "%s" is in contradiction with the required XCount (%d) and YCount (%d)". I am not sure if this is clear to somebody who does not know the internals, though. Decide yourself.

Marcin Wiazowski

2019-02-24 01:15

reporter   ~0114378

What about:

  // There should be XCount + YCount + 2 (for Color and Text) parts of the string
  if Cardinal(parts.Count) <> FSource.XCount + FSource.YCount + 2 then
    raise EStringFormatError.CreateFmt(rsSourceStringFormatError, [FSource.ClassName]);

where

  rsSourceStringFormatError = '%s.DataPoints received string not corresponding to its XCount and YCount values';



Please just apply the patch and tune it in the preferred way.



Please also take a look at "tachart\test\test.lpr" test application - it needs updating, since it started to report errors.

Marcin Wiazowski

2019-02-24 14:59

reporter   ~0114383

Or even better:

  raise EStringFormatError.CreateFmt(rsSourceStringFormatError, [IfThen(FSource.Name <> '', FSource.Name, FSource.ClassName)]);

Marcin Wiazowski

2019-02-24 23:11

reporter   ~0114390

Just an idea: an exception is raised in case of invalid number of string parts - but what about invalid string *contents*?

Currently, for XCount = YCount = 2, the following string is valid:

  1|2|3|4|?|abcd

but the following string is ALSO valid:

  aaa|bbb|ccc|ddd|?|abcd

In this case, aaa / bbb / ccc / ddd parts are silently replaced with zero. Maybe it would be a good idea to raise an exception also in this case? Something like: "The %0:s.DataPoints string "%1:s" contains invalid numeric or color value."

wp

2019-02-24 23:56

developer   ~0114392

Yes. I am also working at validation checks for the datapoints editor. The only non-numeric values allowed will be empty strings, and they should put a NaN into the source instead of a zero.

Marcin Wiazowski

2019-02-25 00:13

reporter  

Bonus.zip (3,086 bytes)

Marcin Wiazowski

2019-02-25 00:13

reporter  

editor.diff (3,567 bytes)   
Index: components/tachart/editors/tadatapointseditor.lfm
===================================================================
--- components/tachart/editors/tadatapointseditor.lfm	(revision 60491)
+++ components/tachart/editors/tadatapointseditor.lfm	(working copy)
@@ -21,17 +21,10 @@
         Alignment = taRightJustify
         Title.Alignment = taCenter
         Title.Font.Style = [fsBold]
-        Title.Caption = 'X'
+        Title.Caption = 'X/Y'
         Width = 63
       end    
       item
-        Alignment = taRightJustify
-        Title.Alignment = taCenter
-        Title.Font.Style = [fsBold]
-        Title.Caption = 'Y'
-        Width = 63
-      end    
-      item
         ButtonStyle = cbsEllipsis
         Title.Alignment = taCenter
         Title.Font.Style = [fsBold]
@@ -56,7 +49,6 @@
       63
       63
       63
-      63
     )
   end
   object ButtonPanel1: TButtonPanel
Index: components/tachart/editors/tadatapointseditor.pas
===================================================================
--- components/tachart/editors/tadatapointseditor.pas	(revision 60491)
+++ components/tachart/editors/tadatapointseditor.pas	(working copy)
@@ -110,28 +110,25 @@
   FYCount := AYCount;
   FDataPoints := ADataPoints;
   sgData.RowCount := Max(ADataPoints.Count + 1, 2);
-  {     wp: What is this good for?
-  for i := sgData.Columns.Count - 1 downto 0 do
-    with sgData.Columns[i].Title do
-      if (Caption[1] = 'Y') and (Caption <> 'Y') then
-        sgData.Columns.Delete(i);
-    }
-  if AXCount > 1 then
-    sgData.Columns[0].Title.Caption := 'X1';
-  if AYCount > 1 then
-    sgData.Columns[1].Title.Caption := 'Y1';
-  for i := 2 to AYCount do
+  for i := 1 to AYCount do
     with sgData.Columns.Add do begin
-      Assign(sgData.Columns[1]);
-      Title.Caption := 'Y' + IntToStr(i);
+      Assign(sgData.Columns[0]); // Columns[0] is a template column
+      if AYCount = 1 then
+        Title.Caption := 'Y'
+      else
+        Title.Caption := 'Y' + IntToStr(i);
       Index := i;
     end;
-  for i := 2 to AXCount do
+  for i := 1 to AXCount do
     with sgData.Columns.Add do begin
-      Assign(sgData.Columns[0]);
-      Title.Caption := 'X' + IntToStr(i);
-      Index := i - 1;
+      Assign(sgData.Columns[0]); // Columns[0] is a template column
+      if AXCount = 1 then
+        Title.Caption := 'X'
+      else
+        Title.Caption := 'X' + IntToStr(i);
+      Index := i;
     end;
+  sgData.Columns.Delete(0); // remove the template column
   for i := 0 to ADataPoints.Count - 1 do
     Split('|' + ADataPoints[i], sgData.Rows[i + 1]);
 
@@ -140,8 +137,13 @@
   for i := 0 to sgData.Columns.Count-1 do
     sgData.Columns[i].Width := w;
 
-  Width := sgData.ColWidths[0] + sgData.Columns.Count * w + 2*sgData.Left +
+{$IFDEF WINDOWS}
+  Width := sgData.ColWidths[0] + 1 + sgData.Columns.Count * w + sgData.Left * 2 +
+    IfThen(sgData.BorderStyle = bsNone, 0, 3);
+{$ELSE}
+  Width := sgData.ColWidths[0] + sgData.Columns.Count * w + sgData.Left * 2 +
     sgData.GridLineWidth * (sgData.Columns.Count-1);
+{$ENDIF}
 end;
 
 procedure TDataPointsEditorForm.miDeleteRowClick(Sender: TObject);
@@ -157,8 +159,8 @@
 procedure TDataPointsEditorForm.FormCreate(Sender: TObject);
 begin
   Caption := desDatapointEditor;
-  sgData.Columns[2].Title.Caption := desColor;
-  sgData.Columns[3].Title.Caption := desText;
+  sgData.Columns[1].Title.Caption := desColor;
+  sgData.Columns[2].Title.Caption := desText;
   miInsertRow.Caption := desInsertRow;
   miDeleteRow.Caption := desDeleteRow;
 end;
editor.diff (3,567 bytes)   

Marcin Wiazowski

2019-02-25 00:15

reporter   ~0114393

Now a useful side-effect: we no longer need to store useless data point's coordinates in LFM! Please take a look at Bonus.zip, at sources' definition in Unit1.lfm. I achieved this just by placing sources on the form, initializing their contents by using DataPoints editor, and then decreasing XCount / YCount in the Object Inspector.

To allow doing this directly in DataPoints editor, I'm attaching editor.diff - it allows proper editing also when XCount or YCount is 0. Some comments:

- There is a comment "What is this good for?" in the code. It seems that, some time ago, DataPoints editor's window has been created once, but then displayed many times - so TDataPointsEditorForm.InitData() has been called many times. In this case, there was a need to reset columns' state first, before adjusting them to current AYCount value. Currently, DataPoints editor's window is created each time from scratch, so this code is useless and is commented out. It can be just removed.

- Under Windows, width of the grid must be calculated differently: in particular, GridLineWidth does NOT enlarge width of the cell - it makes cell's internal area smaller. And "+1" in the attached code is a width of the vertical line, that separates the first (fixed) column from next ones.

wp

2019-02-25 14:22

developer   ~0114407

Neat, applied. The user, however, should be aware that he needs a second source when the associated series is to be rotated in x/y; this can be avoided with a source having x AND y columns with equal values.

Marcin Wiazowski

2019-02-25 18:10

reporter  

patch3.diff (4,120 bytes)   
Index: components/tachart/tacustomsource.pas
===================================================================
--- components/tachart/tacustomsource.pas	(revision 60498)
+++ components/tachart/tacustomsource.pas	(working copy)
@@ -785,25 +785,27 @@
   if FExtentIsValid then exit(FExtent);
   FExtent := EmptyExtent;
 
-  if HasXErrorBars then
-    for i := 0 to Count - 1 do begin
-      GetXErrorBarLimits(i, vhi, vlo);
-      UpdateMinMax(vhi, FExtent.a.X, FExtent.b.X);
-      UpdateMinMax(vlo, FExtent.a.X, FExtent.b.X);
-    end
-  else
-    for i:=0 to Count - 1 do
-      UpdateMinMax(Item[i]^.X, FExtent.a.X, FExtent.b.X);
+  if XCount > 0 then
+    if HasXErrorBars then
+      for i := 0 to Count - 1 do begin
+        GetXErrorBarLimits(i, vhi, vlo);
+        UpdateMinMax(vhi, FExtent.a.X, FExtent.b.X);
+        UpdateMinMax(vlo, FExtent.a.X, FExtent.b.X);
+      end
+    else
+      for i:=0 to Count - 1 do
+        UpdateMinMax(Item[i]^.X, FExtent.a.X, FExtent.b.X);
 
-  if HasYErrorBars then
-    for i := 0 to Count - 1 do begin
-      GetYErrorBarLimits(i, vhi, vlo);
-      UpdateMinMax(vhi, FExtent.a.Y, FExtent.b.Y);
-      UpdateMinMax(vlo, FExtent.a.Y, FExtent.b.Y);
-    end
-  else
-    for i:=0 to Count - 1 do
-      UpdateMinMax(Item[i]^.Y, FExtent.a.Y, FExtent.b.Y);
+  if YCount > 0 then
+    if HasYErrorBars then
+      for i := 0 to Count - 1 do begin
+        GetYErrorBarLimits(i, vhi, vlo);
+        UpdateMinMax(vhi, FExtent.a.Y, FExtent.b.Y);
+        UpdateMinMax(vlo, FExtent.a.Y, FExtent.b.Y);
+      end
+    else
+      for i:=0 to Count - 1 do
+        UpdateMinMax(Item[i]^.Y, FExtent.a.Y, FExtent.b.Y);
 
   FExtentIsValid := true;
   Result := FExtent;
Index: components/tachart/tasources.pas
===================================================================
--- components/tachart/tasources.pas	(revision 60498)
+++ components/tachart/tasources.pas	(working copy)
@@ -387,6 +387,15 @@
     p += 1;
   end;
 
+  function LastPart: String;
+  begin
+    Result := NextPart;
+    while p < parts.Count do begin
+      Result += '|' + parts[p];
+      p += 1;
+    end;
+  end;
+
   function SourceClassString: String;
   begin
     Result := IfThen(FSource.Name <> '', FSource.Name, FSource.ClassName);
@@ -422,8 +431,9 @@
 
   parts := Split(AString);
   try
-    // There must be XCount + YCount + 2 parts of the string (+2 for Color and Text)
-    if (Cardinal(parts.Count) <> FSource.XCount + FSource.YCount + 2) then
+    // There must be XCount + YCount + 2 parts of the string (+2 for Color and Text),
+    // or even more when Text contains '|' character(s)
+    if (Cardinal(parts.Count) < FSource.XCount + FSource.YCount + 2) then
       raise EListSourceStringError.CreateFmt(
         rsListSourceStringFormatError, [SourceClassString, ChopString(AString, 20)]);
 
@@ -441,7 +451,7 @@
       end else
         Y := NaN;
       Color := StrToInt(NextPart);
-      Text := NextPart;
+      Text := LastPart;
     end;
   finally
     parts.Free;
@@ -851,16 +861,35 @@
     end;
 end;
 
+function CompareDataItemY(AItem1, AItem2: Pointer): Integer;
+var
+  i: Integer;
+  item1: PChartDataItem absolute AItem1;
+  item2: PChartDataItem absolute AItem2;
+begin
+  Result := CompareFloat(item1^.Y, item2^.Y);
+  if Result = 0 then
+    for i := 0 to Min(High(item1^.YList), High(item2^.YList)) do begin
+      Result := CompareFloat(item1^.YList[i], item2^.YList[i]);
+      if Result <> 0 then
+        exit;
+    end;
+end;
+
 procedure TListChartSource.Sort;
 begin
-  FData.Sort(@CompareDataItemX);
+  if XCount > 0 then
+    FData.Sort(@CompareDataItemX)
+  else
+  if YCount > 0 then
+    FData.Sort(@CompareDataItemY);
 end;
 
 procedure TListChartSource.UpdateCachesAfterAdd(AX, AY: Double);
 begin
   if FExtentIsValid then begin
-    UpdateMinMax(AX, FExtent.a.X, FExtent.b.X);
-    UpdateMinMax(AY, FExtent.a.Y, FExtent.b.Y);
+    if XCount > 0 then UpdateMinMax(AX, FExtent.a.X, FExtent.b.X);
+    if YCount > 0 then UpdateMinMax(AY, FExtent.a.Y, FExtent.b.Y);
   end;
   if FValuesTotalIsValid then
     FValuesTotal += NumberOr(AY);
patch3.diff (4,120 bytes)   

Marcin Wiazowski

2019-02-25 18:11

reporter  

Texts.zip (2,987 bytes)

Marcin Wiazowski

2019-02-25 18:14

reporter   ~0114417

I'm attaching patch3.diff, that solves three issues:

- adapts sorting code to current TListChartSource possibilities

- adapts extent calculation to current TListChartSource possibilities

- removes a limitation, that was impossible to remove before solving the streaming issue: now data point's text can contain also '|' characters; see the attached Texts.zip.

Marcin Wiazowski

2019-02-25 18:31

reporter   ~0114420

I'd like to ask you for your opinion.

1) When YCount is increased, newly added PChartDataItem^.YList items become zeros. Should they remain zeros or should rather be set to NaN?

2) When YCount is decreased to 0, should PChartDataItem^.Y fields be left as they are or rather set to NaN?

wp

2019-02-25 19:57

developer   ~0114424

> removes a limitation, that was impossible to remove before solving the streaming
> issue: now data point's text can contain also '|' characters

Breaks the "too many parts" condition. Solved it in r60501 by quoting the Text field in this case. This automatically also adds line break support. Extended the DataPoints editor to provide a memo for multi-line texts.

------------

> From patch3.diff: "if XCount > 0"

You don't give up ;-)

-----------

> When YCount is increased, newly added PChartDataItem^.YList items become
> zeros. Should they remain zeros or should rather be set to NaN?

I would prefer NaN because the new Datapoins editor translates this to an empty cell in the grid.

-----------

> When YCount is decreased to 0, should PChartDataItem^.Y fields be left as they
> are or rather set to NaN?

Let's keep the original value until we notice that it is harmful. Data loss should be avoided.

Marcin Wiazowski

2019-02-25 22:36

reporter   ~0114434

> You don't give up ;-)

I just predicted, that you will say "Let's keep the original value until we notice that it is harmful." ;-)

Since the value will not be changed to NaN, "XCount > 0" and "YCount > 0" checks must be present in Extent operations, to avoid referring to non-NaN PChartDataItem^.X and PChartDataItem^.Y values.



New editor for texts is nice.



After a cell's button for Text editing has been added, there is little room for the text itself. Suggestion: make Text column's width 2x larger (and change "sgData.Columns.Count * w" to "sgData.Columns.Count * w + w" in form width calculations).



Some problem still left: it's not possible to use " and | chars in the text together. For example, set the following data point's text in the editor:

  Number of "|" occurrences

save the form, close the form and reopen it: an error will occur. Potential solution: something that uses string list's DelimitedText property?

Marcin Wiazowski

2019-02-27 00:10

reporter   ~0114476

Thanks for adding "Move up" / "Move down" in DataPoints editor.

Issue History

Date Modified Username Field Change
2019-02-20 23:27 wp New Issue
2019-02-20 23:27 wp Status new => assigned
2019-02-20 23:27 wp Assigned To => wp
2019-02-21 03:24 Marcin Wiazowski Note Added: 0114318
2019-02-21 03:24 Marcin Wiazowski File Added: patch1.diff
2019-02-21 17:11 wp Note Added: 0114332
2019-02-21 18:07 Marcin Wiazowski Note Added: 0114333
2019-02-21 18:09 Marcin Wiazowski Note Added: 0114334
2019-02-21 18:15 Marcin Wiazowski Note Added: 0114335
2019-02-21 18:33 wp Note Added: 0114336
2019-02-21 18:34 wp Note Edited: 0114336 View Revisions
2019-02-21 18:36 wp Note Edited: 0114336 View Revisions
2019-02-21 18:39 Marcin Wiazowski Note Added: 0114337
2019-02-22 01:20 Marcin Wiazowski Note Added: 0114344
2019-02-22 09:25 wp Note Added: 0114346
2019-02-22 11:40 Marcin Wiazowski Note Added: 0114347
2019-02-23 04:25 Marcin Wiazowski Note Added: 0114357
2019-02-23 04:25 Marcin Wiazowski File Added: test.diff
2019-02-23 04:25 Marcin Wiazowski File Added: Test.zip
2019-02-23 11:51 wp Note Added: 0114361
2019-02-23 16:47 Marcin Wiazowski Note Added: 0114366
2019-02-23 19:01 wp Note Added: 0114372
2019-02-23 21:46 Marcin Wiazowski Note Added: 0114376
2019-02-23 21:47 Marcin Wiazowski File Added: final.diff
2019-02-23 23:59 wp Note Added: 0114377
2019-02-24 01:15 Marcin Wiazowski Note Added: 0114378
2019-02-24 14:59 Marcin Wiazowski Note Added: 0114383
2019-02-24 23:11 Marcin Wiazowski Note Added: 0114390
2019-02-24 23:56 wp Note Added: 0114392
2019-02-25 00:13 Marcin Wiazowski File Added: Bonus.zip
2019-02-25 00:13 Marcin Wiazowski File Added: editor.diff
2019-02-25 00:15 Marcin Wiazowski Note Added: 0114393
2019-02-25 14:22 wp Note Added: 0114407
2019-02-25 14:33 wp Status assigned => resolved
2019-02-25 14:33 wp Resolution open => fixed
2019-02-25 18:10 Marcin Wiazowski File Added: patch3.diff
2019-02-25 18:11 Marcin Wiazowski File Added: Texts.zip
2019-02-25 18:14 Marcin Wiazowski Note Added: 0114417
2019-02-25 18:31 Marcin Wiazowski Note Added: 0114420
2019-02-25 19:57 wp Note Added: 0114424
2019-02-25 22:36 Marcin Wiazowski Note Added: 0114434
2019-02-26 21:55 wp Relationship added related to 0035155
2019-02-27 00:10 Marcin Wiazowski Note Added: 0114476
2019-03-02 01:27 wp Status resolved => closed