Added support for the 'use alternate font metrics' flag.
[redact.git] / lib / redact / part.rb
1 #--
2 # Copyright (c) 2005 Tilman Sauerbeck (tilman at code-monkey de)
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
23 module Redact
24         class Part
25                 TYPE_RECTANGLE = 1
26                 TYPE_TEXT = 2
27                 TYPE_IMAGE = 3
28                 TYPE_SWALLOW = 4
29
30                 include Comparable
31
32                 attr_reader :collection, :id, :name, :dragable, :clip,
33                             :mouse_events, :repeat_events
34
35                 def initialize(collection, id, name)
36                         @collection = collection
37                         @id = id
38
39                         @name = name.to_str.dup.freeze
40                         @type = TYPE_RECTANGLE
41                         @mouse_events = true
42                         @repeat_events = false
43                         @clip = nil
44                         @dragable = Dragable.new(self)
45
46                         @descriptions = Hash.new do |h, k|
47                                 desc, value = k.split("\0")
48                                 value = value.to_f
49
50                                 h[k] = description_class.new(desc, value)
51                         end
52                 end
53
54                 def <=>(b)
55                         @id <=> b.id
56                 end
57
58                 def mouse_events=(val)
59                         @mouse_events = (val == true)
60                 end
61
62                 def repeat_events=(val)
63                         @repeat_events = (val == true)
64                 end
65
66                 def clip=(part)
67                         if part == self
68                                 raise(ArgumentError, "cannot clip part to itself")
69                         elsif !part.nil? && part.collection != @collection
70                                 raise(ArgumentError, "items not in the same collection")
71                         else
72                                 @clip = part
73                         end
74                 end
75
76                 def description(name = "default", value = 0.0) # :yields: desc
77                         d = @descriptions[desc_key(name, value)]
78
79                         block_given? ? (yield d) : d
80                 end
81
82                 protected
83                 def description_class
84                         Description
85                 end
86
87                 def to_eet_name
88                         "Edje_Part"
89                 end
90
91                 def to_eet_properties
92                         other_desc = @descriptions.dup
93                         other_desc.delete(desc_key("default", 0.0))
94
95                         confine_id = @dragable.confine.nil? ?
96                                          -1 : @dragable.confine.id
97
98                         {"name" => [@name],
99                          "id" => [@id],
100                          "type" => [@type, :char],
101                          "effect" => [0, :char],
102                          "mouse_events" => [@mouse_events],
103                          "repeat_events" => [@repeat_events],
104                          "clip_to_id" => [@clip.nil? ? -1 : @clip.id],
105                          "default_desc" => [description("default", 0.0)],
106                          "other_desc" => [other_desc],
107                          "dragable.x" => [@dragable.enabled[0], :char],
108                          "dragable.step_x" => [@dragable.step[0]],
109                          "dragable.count_x" => [@dragable.count[0]],
110                          "dragable.y" => [@dragable.enabled[1], :char],
111                          "dragable.step_y" => [@dragable.step[1]],
112                          "dragable.count_y" => [@dragable.count[1]],
113                          "dragable.events_id" => [-1],
114                          "dragable.counfine_id" => [confine_id]} # not a typo!
115                 end
116
117                 private
118                 def desc_key(name, value)
119                         name + "\0" + value.to_s
120                 end
121         end
122
123         class SwallowPart < Part
124                 def initialize(collection, id, name)
125                         super
126
127                         @type = TYPE_SWALLOW
128                 end
129         end
130
131         class TextPart < Part
132                 attr_accessor :effect
133
134                 def initialize(collection, id, name)
135                         super
136
137                         @type = TYPE_TEXT
138                         @effect = :none
139                         @use_alternate_font_metrics = false
140                 end
141
142                 def use_alternate_font_metrics=(b)
143                         @use_alternate_font_metrics = (b == true)
144                 end
145
146                 protected
147                 def description_class
148                         TextDescription
149                 end
150
151                 def to_eet_properties
152                         effect = case @effect
153                         when :none: 0
154                         when :plain: 1
155                         when :outline: 2
156                         when :soft_outline: 3
157                         when :shadow: 4
158                         when :soft_shadow: 5
159                         when :outline_shadow: 6
160                         when :outline_soft_shadow: 7
161                         else
162                                 raise(RedactError, "invalid effect value - #{@effect}")
163                         end
164
165                         super.merge!(
166                         {"effect" => [effect, :char],
167                          "use_alternate_font_metrics" => [@use_alternate_font_metrics, :char]})
168                 end
169         end
170
171         class ImagePart < Part
172                 def initialize(collection, id, name)
173                         super
174
175                         @type = TYPE_IMAGE
176                 end
177
178                 protected
179                 def description_class
180                         ImageDescription
181                 end
182         end
183
184         class Dragable
185                 attr_reader :enabled, :step, :count, :confine
186
187                 def initialize(part)
188                         @part= part
189
190                         @enabled = [false, false]
191                         @step = [0, 0]
192                         @count = [0, 0]
193                         @confine = nil
194                 end
195
196                 def confine=(part)
197                         if part == @part
198                                 raise(ArgumentError, "cannot confine part to itself")
199                         elsif !part.nil? && part.collection != @part.collection
200                                 raise(ArgumentError, "items not in the same collection")
201                         else
202                                 @confine = part
203                         end
204                 end
205         end
206
207         class Relation
208                 attr_reader :rel, :to_id, :offset
209
210                 def initialize(rel, offset)
211                         @rel = [rel.to_f, rel.to_f]
212                         @to_id = [-1, -1]
213                         @offset = [offset, offset]
214                 end
215
216                 def set_rel(x, y)
217                         @rel = [x.to_f, y.to_f]
218                 end
219
220                 def set_offset(x, y)
221                         @offset = [x, y]
222                 end
223
224                 def to=(part)
225                         self.set_to(part)
226                 end
227
228                 def set_to(part_x, part_y = part_x)
229                         @to_id = [part_x.nil? ? -1 : part_x.id,
230                                   part_y.nil? ? -1 : part_y.id]
231                 end
232         end
233
234         class Description
235                 attr_reader :rel, :aspect, :step, :visible, :color_class
236                 attr_accessor :aspect_preference
237
238                 def initialize(name = "default", value = 0.0)
239                         @name = name.to_str.dup.freeze
240                         @value = value.freeze
241                         @visible = true
242                         @align = [0.5, 0.5]
243                         @min = [0, 0]
244                         @max = [-1, -1]
245                         @step = [0, 0]
246                         @aspect = [0.0, 0.0]
247                         @aspect_preference = :none
248                         @rel = [Relation.new(0.0, 0), Relation.new(1.0, -1)]
249                         @color = [].fill(255, 0..3)
250                         @color_class = ""
251                 end
252
253                 def inherit(other)
254                         unless other.is_a?(Description)
255                                 raise(ArgumentError, "Cannot inherit from description")
256                         end
257
258                         prot = ["@name", "@value"]
259
260                         (instance_variables - prot).each do |v|
261                                 n = other.instance_variable_get(v.intern)
262                                 n = n.dup rescue n
263                                 instance_variable_set(v.intern, n)
264                         end
265                 end
266
267                 def visible=(v)
268                         @visible = (v == true)
269                 end
270
271                 def color_class=(v)
272                         @color_class = v.to_str.dup
273                 end
274
275                 def set_step(x = 0, y = 0)
276                         @step = [x, y]
277                 end
278
279                 def set_aspect(x = 0.0, y = 0.0)
280                         @aspect = [x, y]
281                 end
282
283                 def set_align(x = 0.5, y = 0.5)
284                         @align = [x, y]
285                 end
286
287                 def set_size(w, h)
288                         set_min(w, h)
289                         set_max(w, h)
290                 end
291
292                 def set_min(w, h)
293                         @min = [w, h]
294                 end
295
296                 def set_max(w, h)
297                         @max = [w, h]
298                 end
299
300                 def color=(c)
301                         @color = parse_hex_color(c)
302                 end
303
304                 protected
305                 def parse_hex_color(c)
306                         md = c.match(/^#?(([[:xdigit:]]{2}){1,4})$/)
307                         if md.nil?
308                                 raise(ArgumentError, "Argument is not a hex string")
309                         end
310
311                         pairs = md.captures.shift.split(/(..)/).delete_if do |item|
312                                 item == ""
313                         end
314
315                         pairs.push("00") while pairs.length < 3
316                         pairs.push("ff") if pairs.length == 3
317
318                         pairs.map { |p| p.hex }
319                 end
320
321                 def to_eet_name
322                         "Edje_Part_Description"
323                 end
324
325                 def to_eet_properties
326                         asp_pref = case @aspect_preference
327                         when :none: 0
328                         when :vertical: 1
329                         when :horizontal: 2
330                         when :both: 3
331                         else
332                                 raise(RedactError, "invalid aspect preference value - " +
333                                       @aspect_preference.to_s)
334                         end
335
336                         {"state.name" => [@name],
337                          "state.value" => [@value, :double],
338                          "visible" => [@visible],
339                          "align.x" => [@align[0], :double],
340                          "align.y" => [@align[1], :double],
341                          "min.w" => [@min[0]],
342                          "min.h" => [@min[1]],
343                          "max.w" => [@max[0]],
344                          "max.h" => [@max[1]],
345                          "step.x" => [@step[0]],
346                          "step.y" => [@step[1]],
347                          "aspect.min" => [@aspect[0], :double],
348                          "aspect.max" => [@aspect[1], :double],
349                          "aspect.prefer" => [asp_pref, :char],
350                          "rel1.relative_x" => [@rel[0].rel[0], :double],
351                          "rel1.relative_y" => [@rel[0].rel[1], :double],
352                          "rel1.offset_x" => [@rel[0].offset[0]],
353                          "rel1.offset_y" => [@rel[0].offset[1]],
354                          "rel1.id_x" => [@rel[0].to_id[0]],
355                          "rel1.id_y" => [@rel[0].to_id[1]],
356                          "rel2.relative_x" => [@rel[1].rel[0], :double],
357                          "rel2.relative_y" => [@rel[1].rel[1], :double],
358                          "rel2.offset_x" => [@rel[1].offset[0]],
359                          "rel2.offset_y" => [@rel[1].offset[1]],
360                          "rel2.id_x" => [@rel[1].to_id[0]],
361                          "rel2.id_y" => [@rel[1].to_id[1]],
362                          "color_class" => [@color_class],
363                          "color.r" => [@color[0], :char],
364                          "color.g" => [@color[1], :char],
365                          "color.b" => [@color[2], :char],
366                          "color.a" => [@color[3], :char],
367
368                          # image properties
369                          "image.id" => [-1],
370                          "image.tween_list" => [nil],
371                          "border.l" => [0],
372                          "border.r" => [0],
373                          "border.t" => [0],
374                          "border.b" => [0],
375                          "border.no_fill" => [false],
376                          "fill.smooth" => [true],
377                          "fill.pos_rel_x" => [0.0, :double],
378                          "fill.pos_abs_x" => [0],
379                          "fill.rel_x" => [1.0, :double],
380                          "fill.abs_x" => [0],
381                          "fill.pos_rel_y" => [0.0, :double],
382                          "fill.pos_abs_y" => [0],
383                          "fill.rel_y" => [1.0, :double],
384                          "fill.abs_y" => [0],
385
386                          # text properties
387                          "color2.r" => [0, :char],
388                          "color2.g" => [0, :char],
389                          "color2.b" => [0, :char],
390                          "color2.a" => [255, :char],
391                          "color3.r" => [0, :char],
392                          "color3.g" => [0, :char],
393                          "color3.b" => [0, :char],
394                          "color3.a" => [128, :char],
395                          "text.text" => [""],
396                          "text.text_class" => [""],
397                          "text.font" => [""],
398                          "text.size" => [0],
399                          "text.fit_x" => [false],
400                          "text.fit_y" => [false],
401                          "text.min_x" => [0],
402                          "text.min_y" => [0],
403                          "text.align.x" => [0.0, :double],
404                          "text.align.y" => [0.0, :double],
405                          "text.id_source" => [-1],
406                          "text.id_text_source" => [-1]}
407                 end
408         end
409
410         class Tween
411                 def initialize(image)
412                         @id = image.id
413                 end
414
415                 protected
416                 def to_eet_name
417                         "Edje_Part_Image_Id"
418                 end
419         end
420
421         class Tweens < Array
422                 def <<(im)
423                         im2 = find_image(im.to_str.strip)
424                         raise(RedactError, "cannot find image - #{im}") if im2.nil?
425
426                         image = EDJE.image_dir.find { |e| e.filename == im2 }
427                         if image.nil?
428                                 image = ImageDirectoryEntry.new(im, im2)
429                                 EDJE.image_dir << image
430                         end
431
432                         super(Tween.new(image))
433                 end
434
435                 private
436                 def find_image(file)
437                         [".", OPTIONS.image_dir].each do |d|
438                                 f2 = File.join(d, file)
439                                 return Pathname.new(f2).cleanpath.to_s if File.file?(f2)
440                         end
441
442                         nil
443                 end
444
445         end
446
447         class ImageDescription < Description
448                 attr_reader :image, :auto_rel, :tweens, :border_fill_middle,
449                             :fill_smooth, :fill_pos_rel, :fill_pos_abs,
450                             :fill_rel, :fill_abs
451
452                 def initialize(name = "default", value = 0.0)
453                         super
454
455                         @image = nil
456                         @tweens = Tweens.new
457                         @border = [0, 0, 0, 0]
458                         @border_fill_middle = true
459
460                         @fill_smooth = true
461                         @fill_pos_rel = [0.0, 0.0]
462                         @fill_pos_abs = [0, 0]
463                         @fill_rel = [1.0, 1.0]
464                         @fill_abs = [0, 0]
465
466                         @auto_rel = false
467                 end
468
469                 def border_fill_middle=(var)
470                         @border_fill_middle = (var == true)
471                 end
472
473                 def image=(im)
474                         im2 = find_image(im.to_str.strip)
475                         raise(RedactError, "cannot find image - #{im}") if im2.nil?
476
477                         return if !@image.nil? && im2 == @image.filename
478
479                         @image = EDJE.image_dir.find { |e| e.filename == im2 }
480                         if @image.nil?
481                                 @image = ImageDirectoryEntry.new(im, im2)
482                                 EDJE.image_dir << @image
483                         end
484
485                         self.auto_rel = @auto_rel
486                 end
487
488                 def auto_rel=(b)
489                         @auto_rel = b
490
491                         if @auto_rel && !@image.nil?
492                                 off = @rel[0].offset
493
494                                 @rel[1].set_rel(0.0, 0.0)
495                                 @rel[1].set_offset(off[0] + @image.image.width - 1,
496                                                    off[1] + @image.image.height - 1)
497                         end
498                 end
499
500                 def set_border(l = 0, r = 0, t = 0, b = 0)
501                         @border = [l, r, t, b]
502                 end
503
504                 def fill_smooth=(v)
505                         @fill_smooth = (v == true)
506                 end
507
508                 def set_fill_pos_rel(x, y)
509                         @fill_pos_rel = [x.to_f, y.to_f]
510                 end
511
512                 def set_fill_pos_abs(x, y)
513                         @fill_pos_abs = [x.to_i, y.to_i]
514                 end
515
516                 def set_fill_rel(x, y)
517                         @fill_rel = [x.to_f, y.to_f]
518                 end
519
520                 def set_fill_abs(x, y)
521                         @fill_abs = [x.to_i, y.to_i]
522                 end
523
524                 protected
525                 def to_eet_properties
526                         super.merge!(
527                         {"image.id" => [@image.nil? ? -1 : @image.id],
528                          "image.tween_list" => [@tweens],
529                          "border.l" => [@border[0]],
530                          "border.r" => [@border[1]],
531                          "border.t" => [@border[2]],
532                          "border.b" => [@border[3]],
533                          "border.no_fill" => [!@border_fill_middle],
534                          "fill.smooth" => [@fill_smooth],
535                          "fill.pos_rel_x" => [@fill_pos_rel[0], :double],
536                          "fill.pos_abs_x" => [@fill_pos_abs[0]],
537                          "fill.rel_x" => [@fill_rel[0], :double],
538                          "fill.abs_x" => [@fill_abs[0]],
539                          "fill.pos_rel_y" => [@fill_pos_rel[1], :double],
540                          "fill.pos_abs_y" => [@fill_pos_abs[1]],
541                          "fill.rel_y" => [@fill_rel[1], :double],
542                          "fill.abs_y" => [@fill_abs[1]]})
543                 end
544
545                 private
546                 def find_image(file)
547                         [".", OPTIONS.image_dir].each do |d|
548                                 f2 = File.join(d, file)
549                                 return Pathname.new(f2).cleanpath.to_s if File.file?(f2)
550                         end
551
552                         nil
553                 end
554         end
555
556         class TextDescription < Description
557                 attr_reader :font, :text, :font_size, :text_class
558
559                 def initialize(name = "default", value = 0.0)
560                         super
561
562                         @outline_color = [0, 0, 0, 255]
563                         @shadow_color = [0, 0, 0, 128]
564                         @text = ""
565                         @text_class = ""
566                         @font = ""
567                         @font_size = 0
568                         @fit = [false, false]
569                         @text_min = [false, false]
570                         @text_max = [false, false]
571                         @text_align = [0.5, 0.5]
572                         @text_id_source = -1
573                         @text_id_text_source = -1
574                 end
575
576                 def text=(v)
577                         @text = v.to_str.dup
578                 end
579
580                 def font_size=(v)
581                         @font_size = v.to_int
582                 end
583
584                 def text_class=(v)
585                         @text_class = v.to_str.dup
586                 end
587
588                 def set_fit(x = false, y = false)
589                         @fit = [x, y]
590                 end
591
592                 def set_text_min(x = false, y = false)
593                         @text_min = [x, y]
594                 end
595
596                 def set_text_max(x = false, y = false)
597                         @text_max = [x, y]
598                 end
599
600                 def set_text_align(x = 0.5, y = 0.5)
601                         @text_align = [x, y]
602                 end
603
604                 def font=(f)
605                         f = f.to_str.strip
606                         md = f.match(/.*\.ttf$/)
607                         unless md.nil?
608                                 f2 = find_font(f)
609                                 raise(RedactError, "cannot find font - #{f}") if f2.nil?
610
611                                 found = EDJE.font_dir.find { |font| font.filename == f2 }
612                                 if found.nil?
613                                         EDJE.font_dir << FontDirectoryEntry.new(f, f2)
614                                         @font = EDJE.font_dir.last.alias
615                                 else
616                                         @font = found.alias
617                                 end
618                         else
619                                 @font = f
620                         end
621                 end
622
623                 def outline_color=(c)
624                         @outline_color = parse_hex_color(c)
625                 end
626
627                 def shadow_color=(c)
628                         @shadow_color = parse_hex_color(c)
629                 end
630
631                 protected
632                 def to_eet_properties
633                         super.merge!(
634                         {"color2.r" => [@outline_color[0], :char],
635                          "color2.g" => [@outline_color[1], :char],
636                          "color2.b" => [@outline_color[2], :char],
637                          "color2.a" => [@outline_color[3], :char],
638                          "color3.r" => [@shadow_color[0], :char],
639                          "color3.g" => [@shadow_color[1], :char],
640                          "color3.b" => [@shadow_color[2], :char],
641                          "color3.a" => [@shadow_color[3], :char],
642                          "text.text" => [@text],
643                          "text.text_class" => [@text_class],
644                          "text.font" => [@font],
645                          "text.size" => [@font_size],
646                          "text.fit_x" => [@fit[0]],
647                          "text.fit_y" => [@fit[1]],
648                          "text.min_x" => [@text_min[0]],
649                          "text.min_y" => [@text_min[1]],
650                          "text.max_x" => [@text_max[0]],
651                          "text.max_y" => [@text_max[1]],
652                          "text.align.x" => [@text_align[0], :double],
653                          "text.align.y" => [@text_align[1], :double],
654                          "text.id_source" => [@text_id_source],
655                          "text.id_text_source" => [@text_id_text_source]})
656                 end
657
658                 private
659                 def find_font(file)
660                         [".", OPTIONS.font_dir].each do |d|
661                                 f2 = File.join(d, file)
662                                 return Pathname.new(f2).cleanpath.to_s if File.file?(f2)
663                         end
664
665                         nil
666                 end
667         end
668 end