File: ReportTableCell.rb

package info (click to toggle)
tj3 3.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,048 kB
  • sloc: ruby: 36,481; javascript: 1,113; sh: 19; makefile: 17
file content (405 lines) | stat: -rw-r--r-- 13,808 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = ReportTableCell.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2024
#               by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

class TaskJuggler

  # This class models the output format independent version of a cell in a
  # TableReport. It belongs to a certain ReportTableLine and
  # ReportTableColumn. Normally a cell contains text on a colored background.
  # By help of the @special variable it can alternatively contain any object
  # the provides the necessary output methods such as to_html.
  class ReportTableCell

    attr_reader :line
    attr_accessor :data, :category, :hidden, :alignment, :padding, :force_string,
                  :text, :tooltip, :showTooltipHint,
                  :iconTooltip,
                  :cellColor, :indent, :icon, :fontSize, :fontColor,
                  :bold, :width,
                  :rows, :columns, :special

    # Create the ReportTableCell object and initialize the attributes to some
    # default values. _line_ is the ReportTableLine this cell belongs to. _text_
    # is the text that should appear in the cell. _headerCell_ is a flag that
    # must be true only for table header cells.
    def initialize(line, query, text = nil, headerCell = false)
      @line = line
      @line.addCell(self) if line

      # Specifies whether this is a header cell or not.
      @headerCell = headerCell
      # A copy of a Query object that is needed to access project data via the
      # query function.
      @query = query ? query.dup : nil
      # The cell textual content. This may be a String or a
      # RichTextIntermediate object.
      self.text = text || ''
      # A custom text for the tooltip.
      @tooltip = nil
      # Determines if the tooltip is triggered by an special hinting icon or
      # the whole cell.
      @showTooltipHint = true
      # The original data of the cell content (optional, nil if not provided)
      @data = nil
      # Determines the background color of the cell.
      @category = nil
      # True of the cell is hidden (because other cells span multiple rows or
      # columns)
      @hidden = false
      # How to horizontally align the cell
      @alignment = :center
      # Horizontal padding between frame and cell content
      @padding = 3
      # Don't convert Strings that look like numbers to String
      @force_string = false
      # Whether or not to indent the cell. If not nil, it is an Integer
      # indicating the indentation level.
      @indent = nil
      # The basename of the icon file
      @icon = nil
      # A custom tooltip for the cell icon
      @iconTooltip = nil
      # Font size of the cell text in pixels
      @fontSize = nil
      # The background color of the cell. Overwrite the @category color.
      @cellColor = nil
      # The color of the cell text font.
      @fontColor = nil
      # True of a bold font is to be used for the cell text.
      @bold = false
      # The width of the column in pixels
      @width = nil
      # The number of rows the cell spans
      @rows = 1
      # The number of columns the cell spans
      @columns = 1
      # Ignore everything and use this reference to generate the output.
      @special = nil
    end

    # Return true if two cells are similar enough so that they can be merged in
    # the report to a single, wider cell. _c_ is the cell to compare this cell
    # with.
    def ==(c)
      @text == c.text &&
      @tooltip == c.tooltip &&
      @alignment == c.alignment &&
      @padding == c.padding &&
      @indent == c.indent &&
      @cellColor == c.cellColor &&
      @category == c.category
    end

    # Turn the abstract cell representation into an HTML element tree.
    def to_html
      return nil if @hidden
      return @special.to_html if @special

      # Determine cell attributes
      attribs = { }
      attribs['rowspan'] = "#{@rows}" if @rows > 1
      attribs['colspan'] = "#{@columns}" if @columns > 1
      attribs['class'] = @category ? @category : 'tabcell'
      style = ''
      style += "background-color: #{@cellColor}; " if @cellColor
      attribs['style'] = style unless style.empty?
      cell = XMLElement.new('td', attribs)

      cell << (table = XMLElement.new('table',
        'class' => @category ? 'tj_table_cell' : 'tj_table_header_cell',
        'cellspacing' => '0', 'style' => cellStyle))
      table << (row = XMLElement.new('tr'))

      calculateIndentation

      # Insert a padding cell for the left side indentation.
      if @leftIndent && @leftIndent > 0
        row << XMLElement.new('td', 'style' => "width:#{@leftIndent}px; ")
      end
      row << cellIcon(cell)

      labelDiv, tooltip = cellLabel
      row << labelDiv

      # Overwrite the tooltip if the user has specified a custom tooltip.
      tooltip = @tooltip if @tooltip
      if tooltip && !tooltip.empty? && !selfcontained
        if @showTooltipHint
          row << (td = XMLElement.new('td'))
          td << XMLElement.new('img',
                               'src' => "#{auxDir}icons/details.png",
                               'class' => 'tj_table_cell_tooltip')
          addHtmlTooltip(tooltip, td, cell)
        else
          addHtmlTooltip(tooltip, cell)
        end
      end

      # Insert a padding cell for the right side indentation.
      if @rightIndent && @rightIndent > 0
        row << XMLElement.new('td', 'style' => "width:#{@rightIndent}px; ")
      end

      cell
    end

    # Add the text content of the cell to an Array of Arrays form of the table.
    def to_csv(csv, columnIdx, lineIdx)
      # We only support left indentation in CSV files as the spaces for right
      # indentation will be disregarded by most applications.
      indent = @indent && @alignment == :left ? '  ' * @indent : ''
      columns = 1
      if @special
        # This is for nested tables. They will be inserted as whole columns
        # in the existing table.
        csv[lineIdx][columnIdx] = nil
        columns = @special.to_csv(csv, columnIdx)
      else
        cell =
          if @data && @data.is_a?(String)
            @data
          elsif @text
            if @text.respond_to?('functionHandler')
              @text.setQuery(@query)
            end
            str = @text.to_s
            # Remove any trailing line breaks. These don't really make much
            # sense in CSV files.
            while str[-1] == ?\n
              str.chomp!
            end
            str
          end

        # Try to convert numbers and other types to their native Ruby type if
        # they are supported by CSVFile.
        native = @force_string ? cell : CSVFile.strToNative(cell)

        # Only for String objects, we add the indentation.
        csv[lineIdx][columnIdx] = (native.is_a?(String) && !@force_string ?
                                   indent + native : native)
      end

      return columns
    end

    private

    def selfcontained
      @line && @line.table.selfcontained
    end

    def auxDir
      @line ? @line.table.auxDir : nil
    end

    def calculateIndentation
      # In tree sorting mode, some cells have to be indented to reflect the
      # tree nesting structure. The indentation is achieved with padding cells
      # and needs to be applied to the proper side depending on the alignment.
      @leftIndent = @rightIndent = 0
      if @indent && @alignment != :center
        if @alignment == :left
          @leftIndent = @indent * 8
        elsif @alignment == :right
          @rightIndent = (@line.table.maxIndent - @indent) * 8
        end
      end
    end

    # Determine cell style
    def cellStyle
      style = "text-align:#{@alignment.to_s}; "
      if @line && @line.table.equiLines
        style += "height:#{@line.height - 7}px; "
      end

      style
    end

    def cellIcon(cell)
      if @icon && !selfcontained
        td = XMLElement.new('td', 'class' => 'tj_table_cell_icon')
        td << XMLElement.new('img', 'src' => "#{auxDir}icons/#{@icon}.png",
                                    'alt' => "Icon")
        addHtmlTooltip(@iconTooltip, td, cell)
        return td
      end

      nil
    end

    def cellLabel
      # If we have a RichText content and a width limit, we enable line
      # wrapping.
      #                Overfl. Wrap. Height Width
      # Fixed Height:    x      -      x     -
      # Fixed Width:     x      x      -     x
      # Both:            x      -      x     x
      # None:            -      x      -     -
      fixedHeight = @line && @line.table.equiLines
      fixedWidth = !@width.nil?
      style = ''
      style += "overflow:hidden; " if fixedHeight || fixedWidth
      style += "white-space:#{fixedWidth && !fixedHeight ?
                              'normal' : 'nowrap'}; "
      if fixedHeight && !fixedWidth
        style += "height:#{@line.height - 3}px; "
      end
      style += 'font-weight:bold; ' if @bold
      style += "font-size: #{@fontSize}px; " if fontSize
      if @fontColor
        style += "color:#{@fontColor}; "
      end

      return nil, nil if @text.nil? || @text.empty?

      tooltip = nil

      # @text can be a String or a RichText (with or without embedded
      # queries). To find out if @text has multiple lines, we need to expand
      # it and convert it to a plain text again.
      textAsString =
        if @text.is_a?(RichTextIntermediate)
          # @text is a RichText.
          if @text.respond_to?('functionHandler')
            @text.setQuery(@query)
          end
          @text.to_s
        else
          @text
        end

      return nil, nil if textAsString.empty?

      if @width
        # We have 4 pixels padding on each side of the cell.
        labelWidth = @width - 8
        labelWidth -= @leftIndent if @leftIndent
        labelWidth -= @rightIndent if @rightIndent
        if !selfcontained
          # The icons are 20 pixels width including padding.
          labelWidth -= 20 if @icon
          labelWidth -= 20 if tooltip || @tooltip
        end
      else
        labelWidth = nil
      end
      shortText, singleLine = shortVersion(textAsString, labelWidth)

      if (@line && @line.table.equiLines && (!singleLine || @width )) &&
          !@headerCell
        # The cell is size-limited. We only put a shortened plain-text version
        # in the cell and provide the full content via a tooltip.
        # Header cells are never shortened.
        tooltip = @text if shortText != textAsString
        tl = XMLText.new(shortText)
      else
        tl = (@text.is_a?(RichTextIntermediate) ? @text.to_html :
                                                  XMLText.new(@text))
      end

      if labelWidth
        style += "min-width: #{labelWidth}px; max-width: #{labelWidth}px; "
      end

      td = XMLElement.new('td', 'class' => 'tj_table_cell_label',
                                'style' => style)
      td << tl

      return td, tooltip
    end

    # Convert a RichText String into a small one-line plain text
    # version that fits the column.
    def shortVersion(itext, width)
      text = itext.to_s
      singleLine = true
      modified = false
      if text.include?("\n")
        text = text[0, text.index("\n")]
        singleLine = false
        modified = true
      end
      if width
        widthWithoutIcon = width - 20
        # Assuming an average character width of 7 pixels
        maxChars = widthWithoutIcon / 7
        if text.length > maxChars
          if maxChars > 0
            text = text[0, maxChars]
          else
            text = ''
          end
          modified = true
        end
      end
      # Add three dots to show that there is more info available.
      text += "..." if modified
      [ text, singleLine ]
    end

    def addHtmlTooltip(tooltip, trigger, hook = nil)
      return unless tooltip && !tooltip.empty? && !selfcontained

      hook = trigger if hook.nil?
      if tooltip.respond_to?('functionHandler')
        tooltip.setQuery(@query)
      end
      if @query
        @query.attributeId = 'name'
        @query.process
        title = @query.to_s
      else
        title = ''
      end
      trigger['onclick'] = "TagToTip('ID#{trigger.object_id}', " +
                           "TITLE, '#{title.gsub(/'/, '&apos;')}')"
      trigger['style'] = trigger['style'] ? trigger['style'] : 'cursor:help; '
      hook << (ltDiv = XMLElement.new('div',
                                      'class' => 'tj_tooltip_box',
                                       'style' => 'cursor:help',
                                       'id' => "ID#{trigger.object_id}"))
      ltDiv << (tooltip.respond_to?('to_html') ? tooltip.to_html :
                                                 XMLText.new(tooltip))
    end

  end

  # This class is used to model cells that are just placeholders for a line of
  # an embedded ReportTable.
  class PlaceHolderCell

    # Create a new placeholder cell. _line_ is the line that this cell belongs
    # to. _embeddedLine_ is the ReportTableLine that is embedded in this cell.
    def initialize(line, embeddedLine)
      @line = line
      @line.addCell(self) if line
      @embeddedLine = embeddedLine
    end

    # Add the current cell to the _csv_ CSV Arrays. _columnIdx_ is the start
    # column in the _csv_. _lineIdx_ is the index of the current line. The
    # return value is the number of added cells.
    def to_csv(csv, columnIdx, lineIdx)
      @embeddedLine.to_csv(csv, columnIdx, lineIdx)
    end

    def to_html
      nil
    end

  end

end