From: adam.b Date: Fri, 23 Oct 2020 10:46:01 +0000 (+0100) Subject: [dali_1.9.35] Merge branch 'devel/master' X-Git-Tag: dali_2.0.7~4^2~8 X-Git-Url: http://review.tizen.org/git/?p=platform%2Fcore%2Fuifw%2Fdali-toolkit.git;a=commitdiff_plain;h=f41e5a967dd08e8de78a0822a5361331a965cd28;hp=eac3e6ee30183868f1af12addd94e7f2fc467ed7 [dali_1.9.35] Merge branch 'devel/master' Change-Id: I3a8a0787c378b25b10d24b5d654a88e6ef620a17 --- diff --git a/automated-tests/README.md b/automated-tests/README.md index c062b8a..590571d 100644 --- a/automated-tests/README.md +++ b/automated-tests/README.md @@ -123,11 +123,29 @@ To execute a subset of tests, you can run individual test sets, e.g. ./execute.sh dali-toolkit -To get coverage output (you need to first build dali libraries with +To get full coverage output (you need to first build dali libraries with --coverage), run ./coverage.sh +To check the coverage of your patch, (the build server uses its own copy +of these scripts), you can use + + ./patch-coverage.pl -q [diff-spec] + +to get a summary, or + + ./patch-coverage.pl [diff-spec] + +to get textual output, or + + ./patch-coverage.pl -o out.html [diff-spec] + +to get HTML output (used by build server). + +diff-spec is any refspec accepted by git-diff. If it's left out, it creates +a refspec to the latest commit, or uses the index/working tree. + Testing on target ================= diff --git a/automated-tests/patch-coverage.pl b/automated-tests/patch-coverage.pl index c5d9083..702dc0b 100755 --- a/automated-tests/patch-coverage.pl +++ b/automated-tests/patch-coverage.pl @@ -19,33 +19,59 @@ use strict; use Git; use Getopt::Long; use Error qw(:try); -use HTML::Element; use Pod::Usage; use File::Basename; -#use Data::Dumper; use File::stat; use Scalar::Util qw /looks_like_number/; use Cwd qw /getcwd/; use Term::ANSIColor qw(:constants); +use Data::Dumper; -# Program to run gcov on files in patch (that are in source dirs - needs to be dali-aware). +# Dali specific program to run lcov and parse output for files in patch -# A) Get patch -# B) Remove uninteresting files -# C) Find matching gcno/gcda files -# D) Copy and rename them to match source prefix (i.e. strip library name off front) -# E) Generate patch output with covered/uncovered lines marked in green/red -# F) Generate coverage data for changed lines -# G) Exit status should be 0 for high coverage (90% line coverage for all new/changed lines) +# A) Generate lcov output from lib & test cases +# B) Get patch using git diff +# C) Generate patch output with covered/uncovered lines marked in green/red +# D) Generate coverage data for changed lines +# E) Exit status should be 0 for high coverage (90% line coverage for all new/changed lines) # or 1 for low coverage # Sources for conversion of gcno/gcda files: -# ~/bin/lcov +# /usr/bin/lcov & genhtml # Python git-coverage (From http://stef.thewalter.net/git-coverage-useful-code-coverage.html) +# From genhtml: +sub read_info_file($); +sub get_info_entry($); +sub set_info_entry($$$$$$$$$;$$$$$$); +sub combine_info_entries($$$); +sub combine_info_files($$); +sub compress_brcount($); +sub brcount_to_db($); +sub db_to_brcount($;$); +sub brcount_db_combine($$$); +sub add_counts($$); +sub info(@); + our $repo = Git->repository(); -our $debug=0; +our $debug=1; our $pd_debug=0; +our $root; +our %info_data; # Hash containing all data from .info files + +# Settings from genhtml: +our $func_coverage; # If set, generate function coverage statistics +our $no_func_coverage; # Disable func_coverage +our $br_coverage; # If set, generate branch coverage statistics +our $no_br_coverage; # Disable br_coverage +our $sort = 1; # If set, provide directory listings with sorted entries +our $no_sort; # Disable sort + +# Branch data combination types +our $BR_SUB = 0; +our $BR_ADD = 1; + +# Command line options our $opt_cached; our $opt_help; our $opt_output; @@ -64,6 +90,793 @@ GetOptions( %longOptions ) or pod2usage(2); pod2usage(1) if $opt_help; +# From genhtml: +# +# read_info_file(info_filename) +# +# Read in the contents of the .info file specified by INFO_FILENAME. Data will +# be returned as a reference to a hash containing the following mappings: +# +# %result: for each filename found in file -> \%data +# +# %data: "test" -> \%testdata +# "sum" -> \%sumcount +# "func" -> \%funcdata +# "found" -> $lines_found (number of instrumented lines found in file) +# "hit" -> $lines_hit (number of executed lines in file) +# "f_found" -> $fn_found (number of instrumented functions found in file) +# "f_hit" -> $fn_hit (number of executed functions in file) +# "b_found" -> $br_found (number of instrumented branches found in file) +# "b_hit" -> $br_hit (number of executed branches in file) +# "check" -> \%checkdata +# "testfnc" -> \%testfncdata +# "sumfnc" -> \%sumfnccount +# "testbr" -> \%testbrdata +# "sumbr" -> \%sumbrcount +# +# %testdata : name of test affecting this file -> \%testcount +# %testfncdata: name of test affecting this file -> \%testfnccount +# %testbrdata: name of test affecting this file -> \%testbrcount +# +# %testcount : line number -> execution count for a single test +# %testfnccount: function name -> execution count for a single test +# %testbrcount : line number -> branch coverage data for a single test +# %sumcount : line number -> execution count for all tests +# %sumfnccount : function name -> execution count for all tests +# %sumbrcount : line number -> branch coverage data for all tests +# %funcdata : function name -> line number +# %checkdata : line number -> checksum of source code line +# $brdata : vector of items: block, branch, taken +# +# Note that .info file sections referring to the same file and test name +# will automatically be combined by adding all execution counts. +# +# Note that if INFO_FILENAME ends with ".gz", it is assumed that the file +# is compressed using GZIP. If available, GUNZIP will be used to decompress +# this file. +# +# Die on error. +# +sub read_info_file($) +{ + my $tracefile = $_[0]; # Name of tracefile + my %result; # Resulting hash: file -> data + my $data; # Data handle for current entry + my $testdata; # " " + my $testcount; # " " + my $sumcount; # " " + my $funcdata; # " " + my $checkdata; # " " + my $testfncdata; + my $testfnccount; + my $sumfnccount; + my $testbrdata; + my $testbrcount; + my $sumbrcount; + my $line; # Current line read from .info file + my $testname; # Current test name + my $filename; # Current filename + my $hitcount; # Count for lines hit + my $count; # Execution count of current line + my $negative; # If set, warn about negative counts + my $changed_testname; # If set, warn about changed testname + my $line_checksum; # Checksum of current line + my $notified_about_relative_paths; + local *INFO_HANDLE; # Filehandle for .info file + + info("Reading data file $tracefile\n"); + + # Check if file exists and is readable + stat($tracefile); + if (!(-r _)) + { + die("ERROR: cannot read file $tracefile!\n"); + } + + # Check if this is really a plain file + if (!(-f _)) + { + die("ERROR: not a plain file: $tracefile!\n"); + } + + # Check for .gz extension + if ($tracefile =~ /\.gz$/) + { + # Check for availability of GZIP tool + system_no_output(1, "gunzip" ,"-h") + and die("ERROR: gunzip command not available!\n"); + + # Check integrity of compressed file + system_no_output(1, "gunzip", "-t", $tracefile) + and die("ERROR: integrity check failed for ". + "compressed file $tracefile!\n"); + + # Open compressed file + open(INFO_HANDLE, "-|", "gunzip -c '$tracefile'") + or die("ERROR: cannot start gunzip to decompress ". + "file $tracefile!\n"); + } + else + { + # Open decompressed file + open(INFO_HANDLE, "<", $tracefile) + or die("ERROR: cannot read file $tracefile!\n"); + } + + $testname = ""; + while () + { + chomp($_); + $line = $_; + + # Switch statement + foreach ($line) + { + /^TN:([^,]*)(,diff)?/ && do + { + # Test name information found + $testname = defined($1) ? $1 : ""; + if ($testname =~ s/\W/_/g) + { + $changed_testname = 1; + } + $testname .= $2 if (defined($2)); + last; + }; + + /^[SK]F:(.*)/ && do + { + # Filename information found + # Retrieve data for new entry + $filename = File::Spec->rel2abs($1, $root); + + if (!File::Spec->file_name_is_absolute($1) && + !$notified_about_relative_paths) + { + info("Resolved relative source file ". + "path \"$1\" with CWD to ". + "\"$filename\".\n"); + $notified_about_relative_paths = 1; + } + + $data = $result{$filename}; + ($testdata, $sumcount, $funcdata, $checkdata, + $testfncdata, $sumfnccount, $testbrdata, + $sumbrcount) = + get_info_entry($data); + + if (defined($testname)) + { + $testcount = $testdata->{$testname}; + $testfnccount = $testfncdata->{$testname}; + $testbrcount = $testbrdata->{$testname}; + } + else + { + $testcount = {}; + $testfnccount = {}; + $testbrcount = {}; + } + last; + }; + + /^DA:(\d+),(-?\d+)(,[^,\s]+)?/ && do + { + # Fix negative counts + $count = $2 < 0 ? 0 : $2; + if ($2 < 0) + { + $negative = 1; + } + # Execution count found, add to structure + # Add summary counts + $sumcount->{$1} += $count; + + # Add test-specific counts + if (defined($testname)) + { + $testcount->{$1} += $count; + } + + # Store line checksum if available + if (defined($3)) + { + $line_checksum = substr($3, 1); + + # Does it match a previous definition + if (defined($checkdata->{$1}) && + ($checkdata->{$1} ne + $line_checksum)) + { + die("ERROR: checksum mismatch ". + "at $filename:$1\n"); + } + + $checkdata->{$1} = $line_checksum; + } + last; + }; + + /^FN:(\d+),([^,]+)/ && do + { + last if (!$func_coverage); + + # Function data found, add to structure + $funcdata->{$2} = $1; + + # Also initialize function call data + if (!defined($sumfnccount->{$2})) { + $sumfnccount->{$2} = 0; + } + if (defined($testname)) + { + if (!defined($testfnccount->{$2})) { + $testfnccount->{$2} = 0; + } + } + last; + }; + + /^FNDA:(\d+),([^,]+)/ && do + { + last if (!$func_coverage); + # Function call count found, add to structure + # Add summary counts + $sumfnccount->{$2} += $1; + + # Add test-specific counts + if (defined($testname)) + { + $testfnccount->{$2} += $1; + } + last; + }; + + /^BRDA:(\d+),(\d+),(\d+),(\d+|-)/ && do { + # Branch coverage data found + my ($line, $block, $branch, $taken) = + ($1, $2, $3, $4); + + last if (!$br_coverage); + $sumbrcount->{$line} .= + "$block,$branch,$taken:"; + + # Add test-specific counts + if (defined($testname)) { + $testbrcount->{$line} .= + "$block,$branch,$taken:"; + } + last; + }; + + /^end_of_record/ && do + { + # Found end of section marker + if ($filename) + { + # Store current section data + if (defined($testname)) + { + $testdata->{$testname} = + $testcount; + $testfncdata->{$testname} = + $testfnccount; + $testbrdata->{$testname} = + $testbrcount; + } + + set_info_entry($data, $testdata, + $sumcount, $funcdata, + $checkdata, $testfncdata, + $sumfnccount, + $testbrdata, + $sumbrcount); + $result{$filename} = $data; + last; + } + }; + + # default + last; + } + } + close(INFO_HANDLE); + + # Calculate lines_found and lines_hit for each file + foreach $filename (keys(%result)) + { + $data = $result{$filename}; + + ($testdata, $sumcount, undef, undef, $testfncdata, + $sumfnccount, $testbrdata, $sumbrcount) = + get_info_entry($data); + + # Filter out empty files + if (scalar(keys(%{$sumcount})) == 0) + { + delete($result{$filename}); + next; + } + # Filter out empty test cases + foreach $testname (keys(%{$testdata})) + { + if (!defined($testdata->{$testname}) || + scalar(keys(%{$testdata->{$testname}})) == 0) + { + delete($testdata->{$testname}); + delete($testfncdata->{$testname}); + } + } + + $data->{"found"} = scalar(keys(%{$sumcount})); + $hitcount = 0; + + foreach (keys(%{$sumcount})) + { + if ($sumcount->{$_} > 0) { $hitcount++; } + } + + $data->{"hit"} = $hitcount; + + # Get found/hit values for function call data + $data->{"f_found"} = scalar(keys(%{$sumfnccount})); + $hitcount = 0; + + foreach (keys(%{$sumfnccount})) { + if ($sumfnccount->{$_} > 0) { + $hitcount++; + } + } + $data->{"f_hit"} = $hitcount; + + # Combine branch data for the same branches + (undef, $data->{"b_found"}, $data->{"b_hit"}) = compress_brcount($sumbrcount); + foreach $testname (keys(%{$testbrdata})) { + compress_brcount($testbrdata->{$testname}); + } + } + + if (scalar(keys(%result)) == 0) + { + die("ERROR: no valid records found in tracefile $tracefile\n"); + } + if ($negative) + { + warn("WARNING: negative counts found in tracefile ". + "$tracefile\n"); + } + if ($changed_testname) + { + warn("WARNING: invalid characters removed from testname in ". + "tracefile $tracefile\n"); + } + + return(\%result); +} + +sub print_simplified_info +{ + for my $key (keys(%info_data)) + { + print "K $key: \n"; + my $sumcountref = $info_data{$key}->{"sum"}; + for my $line (sort{$a<=>$b}(keys(%$sumcountref))) + { + print "L $line: $sumcountref->{$line}\n"; + } + } +} + +# From genhtml: +# +# get_info_entry(hash_ref) +# +# Retrieve data from an entry of the structure generated by read_info_file(). +# Return a list of references to hashes: +# (test data hash ref, sum count hash ref, funcdata hash ref, checkdata hash +# ref, testfncdata hash ref, sumfnccount hash ref, lines found, lines hit, +# functions found, functions hit) +# + +sub get_info_entry($) +{ + my $testdata_ref = $_[0]->{"test"}; + my $sumcount_ref = $_[0]->{"sum"}; + my $funcdata_ref = $_[0]->{"func"}; + my $checkdata_ref = $_[0]->{"check"}; + my $testfncdata = $_[0]->{"testfnc"}; + my $sumfnccount = $_[0]->{"sumfnc"}; + my $testbrdata = $_[0]->{"testbr"}; + my $sumbrcount = $_[0]->{"sumbr"}; + my $lines_found = $_[0]->{"found"}; + my $lines_hit = $_[0]->{"hit"}; + my $fn_found = $_[0]->{"f_found"}; + my $fn_hit = $_[0]->{"f_hit"}; + my $br_found = $_[0]->{"b_found"}; + my $br_hit = $_[0]->{"b_hit"}; + + return ($testdata_ref, $sumcount_ref, $funcdata_ref, $checkdata_ref, + $testfncdata, $sumfnccount, $testbrdata, $sumbrcount, + $lines_found, $lines_hit, $fn_found, $fn_hit, + $br_found, $br_hit); +} + + +# From genhtml: +# +# set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref, +# checkdata_ref, testfncdata_ref, sumfcncount_ref, +# testbrdata_ref, sumbrcount_ref[,lines_found, +# lines_hit, f_found, f_hit, $b_found, $b_hit]) +# +# Update the hash referenced by HASH_REF with the provided data references. +# + +sub set_info_entry($$$$$$$$$;$$$$$$) +{ + my $data_ref = $_[0]; + + $data_ref->{"test"} = $_[1]; + $data_ref->{"sum"} = $_[2]; + $data_ref->{"func"} = $_[3]; + $data_ref->{"check"} = $_[4]; + $data_ref->{"testfnc"} = $_[5]; + $data_ref->{"sumfnc"} = $_[6]; + $data_ref->{"testbr"} = $_[7]; + $data_ref->{"sumbr"} = $_[8]; + + if (defined($_[9])) { $data_ref->{"found"} = $_[9]; } + if (defined($_[10])) { $data_ref->{"hit"} = $_[10]; } + if (defined($_[11])) { $data_ref->{"f_found"} = $_[11]; } + if (defined($_[12])) { $data_ref->{"f_hit"} = $_[12]; } + if (defined($_[13])) { $data_ref->{"b_found"} = $_[13]; } + if (defined($_[14])) { $data_ref->{"b_hit"} = $_[14]; } +} + +# From genhtml: +# +# combine_info_entries(entry_ref1, entry_ref2, filename) +# +# Combine .info data entry hashes referenced by ENTRY_REF1 and ENTRY_REF2. +# Return reference to resulting hash. +# +sub combine_info_entries($$$) +{ + my $entry1 = $_[0]; # Reference to hash containing first entry + my $testdata1; + my $sumcount1; + my $funcdata1; + my $checkdata1; + my $testfncdata1; + my $sumfnccount1; + my $testbrdata1; + my $sumbrcount1; + + my $entry2 = $_[1]; # Reference to hash containing second entry + my $testdata2; + my $sumcount2; + my $funcdata2; + my $checkdata2; + my $testfncdata2; + my $sumfnccount2; + my $testbrdata2; + my $sumbrcount2; + + my %result; # Hash containing combined entry + my %result_testdata; + my $result_sumcount = {}; + my $result_funcdata; + my $result_testfncdata; + my $result_sumfnccount; + my $result_testbrdata; + my $result_sumbrcount; + my $lines_found; + my $lines_hit; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; + + my $testname; + my $filename = $_[2]; + + # Retrieve data + ($testdata1, $sumcount1, $funcdata1, $checkdata1, $testfncdata1, + $sumfnccount1, $testbrdata1, $sumbrcount1) = get_info_entry($entry1); + ($testdata2, $sumcount2, $funcdata2, $checkdata2, $testfncdata2, + $sumfnccount2, $testbrdata2, $sumbrcount2) = get_info_entry($entry2); + +# # Merge checksums +# $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename); +# +# # Combine funcdata +# $result_funcdata = merge_func_data($funcdata1, $funcdata2, $filename); +# +# # Combine function call count data +# $result_testfncdata = add_testfncdata($testfncdata1, $testfncdata2); +# ($result_sumfnccount, $fn_found, $fn_hit) = +# add_fnccount($sumfnccount1, $sumfnccount2); +# +# # Combine branch coverage data +# $result_testbrdata = add_testbrdata($testbrdata1, $testbrdata2); +# ($result_sumbrcount, $br_found, $br_hit) = +# combine_brcount($sumbrcount1, $sumbrcount2, $BR_ADD); +# + # Combine testdata + foreach $testname (keys(%{$testdata1})) + { + if (defined($testdata2->{$testname})) + { + # testname is present in both entries, requires + # combination + ($result_testdata{$testname}) = + add_counts($testdata1->{$testname}, + $testdata2->{$testname}); + } + else + { + # testname only present in entry1, add to result + $result_testdata{$testname} = $testdata1->{$testname}; + } + + # update sum count hash + ($result_sumcount, $lines_found, $lines_hit) = + add_counts($result_sumcount, + $result_testdata{$testname}); + } + + foreach $testname (keys(%{$testdata2})) + { + # Skip testnames already covered by previous iteration + if (defined($testdata1->{$testname})) { next; } + + # testname only present in entry2, add to result hash + $result_testdata{$testname} = $testdata2->{$testname}; + + # update sum count hash + ($result_sumcount, $lines_found, $lines_hit) = + add_counts($result_sumcount, + $result_testdata{$testname}); + } + + # Calculate resulting sumcount + + # Store result + set_info_entry(\%result, \%result_testdata, $result_sumcount, + $result_funcdata, $checkdata1, $result_testfncdata, + $result_sumfnccount, $result_testbrdata, + $result_sumbrcount, $lines_found, $lines_hit, + $fn_found, $fn_hit, $br_found, $br_hit); + + return(\%result); +} + +# From genhtml: +# +# combine_info_files(info_ref1, info_ref2) +# +# Combine .info data in hashes referenced by INFO_REF1 and INFO_REF2. Return +# reference to resulting hash. +# +sub combine_info_files($$) +{ + my %hash1 = %{$_[0]}; + my %hash2 = %{$_[1]}; + my $filename; + + foreach $filename (keys(%hash2)) + { + if ($hash1{$filename}) + { + # Entry already exists in hash1, combine them + $hash1{$filename} = + combine_info_entries($hash1{$filename}, + $hash2{$filename}, + $filename); + } + else + { + # Entry is unique in both hashes, simply add to + # resulting hash + $hash1{$filename} = $hash2{$filename}; + } + } + + return(\%hash1); +} + +# From genhtml: +# +# add_counts(data1_ref, data2_ref) +# +# DATA1_REF and DATA2_REF are references to hashes containing a mapping +# +# line number -> execution count +# +# Return a list (RESULT_REF, LINES_FOUND, LINES_HIT) where RESULT_REF +# is a reference to a hash containing the combined mapping in which +# execution counts are added. +# +sub add_counts($$) +{ + my $data1_ref = $_[0]; # Hash 1 + my $data2_ref = $_[1]; # Hash 2 + my %result; # Resulting hash + my $line; # Current line iteration scalar + my $data1_count; # Count of line in hash1 + my $data2_count; # Count of line in hash2 + my $found = 0; # Total number of lines found + my $hit = 0; # Number of lines with a count > 0 + + foreach $line (keys(%$data1_ref)) + { + $data1_count = $data1_ref->{$line}; + $data2_count = $data2_ref->{$line}; + + # Add counts if present in both hashes + if (defined($data2_count)) { $data1_count += $data2_count; } + + # Store sum in %result + $result{$line} = $data1_count; + + $found++; + if ($data1_count > 0) { $hit++; } + } + + # Add lines unique to data2_ref + foreach $line (keys(%$data2_ref)) + { + # Skip lines already in data1_ref + if (defined($data1_ref->{$line})) { next; } + + # Copy count from data2_ref + $result{$line} = $data2_ref->{$line}; + + $found++; + if ($result{$line} > 0) { $hit++; } + } + + return (\%result, $found, $hit); +} + + +# From genhtml: +sub compress_brcount($) +{ + my ($brcount) = @_; + my $db; + + $db = brcount_to_db($brcount); + return db_to_brcount($db, $brcount); +} + +# +# brcount_to_db(brcount) +# +# Convert brcount data to the following format: +# +# db: line number -> block hash +# block hash: block number -> branch hash +# branch hash: branch number -> taken value +# + +sub brcount_to_db($) +{ + my ($brcount) = @_; + my $line; + my $db; + + # Add branches to database + foreach $line (keys(%{$brcount})) { + my $brdata = $brcount->{$line}; + + foreach my $entry (split(/:/, $brdata)) { + my ($block, $branch, $taken) = split(/,/, $entry); + my $old = $db->{$line}->{$block}->{$branch}; + + if (!defined($old) || $old eq "-") { + $old = $taken; + } elsif ($taken ne "-") { + $old += $taken; + } + + $db->{$line}->{$block}->{$branch} = $old; + } + } + + return $db; +} + + +# +# db_to_brcount(db[, brcount]) +# +# Convert branch coverage data back to brcount format. If brcount is specified, +# the converted data is directly inserted in brcount. +# + +sub db_to_brcount($;$) +{ + my ($db, $brcount) = @_; + my $line; + my $br_found = 0; + my $br_hit = 0; + + # Convert database back to brcount format + foreach $line (sort({$a <=> $b} keys(%{$db}))) { + my $ldata = $db->{$line}; + my $brdata; + my $block; + + foreach $block (sort({$a <=> $b} keys(%{$ldata}))) { + my $bdata = $ldata->{$block}; + my $branch; + + foreach $branch (sort({$a <=> $b} keys(%{$bdata}))) { + my $taken = $bdata->{$branch}; + + $br_found++; + $br_hit++ if ($taken ne "-" && $taken > 0); + $brdata .= "$block,$branch,$taken:"; + } + } + $brcount->{$line} = $brdata; + } + + return ($brcount, $br_found, $br_hit); +} + + +# +# brcount_db_combine(db1, db2, op) +# +# db1 := db1 op db2, where +# db1, db2: brcount data as returned by brcount_to_db +# op: one of $BR_ADD and BR_SUB +# +sub brcount_db_combine($$$) +{ + my ($db1, $db2, $op) = @_; + + foreach my $line (keys(%{$db2})) { + my $ldata = $db2->{$line}; + + foreach my $block (keys(%{$ldata})) { + my $bdata = $ldata->{$block}; + + foreach my $branch (keys(%{$bdata})) { + my $taken = $bdata->{$branch}; + my $new = $db1->{$line}->{$block}->{$branch}; + + if (!defined($new) || $new eq "-") { + $new = $taken; + } elsif ($taken ne "-") { + if ($op == $BR_ADD) { + $new += $taken; + } elsif ($op == $BR_SUB) { + $new -= $taken; + $new = 0 if ($new < 0); + } + } + + $db1->{$line}->{$block}->{$branch} = $new; + } + } + } +} + +# From genhtml +sub info(@) +{ + if($debug) + { + # Print info string + printf(@_); + } +} + +# NEW STUFF + ## Format per file, repeated, no linebreak # # index c1..c2 c3 @@ -78,6 +891,12 @@ pod2usage(1) if $opt_help; # 3 lines of context # # output: +# : source / header files in dali/dali-toolkit +# \%filter: -> \%filedata +# %filedata: "patch" -> \@checklines +# "b_lines" -> \%b_lines +# @checklines: vector of \[start, length] # line numbers of new/modified lines +# %b_lines: -> patch line in b-file. sub parse_diff { my $patchref = shift; @@ -184,7 +1003,7 @@ sub parse_diff } } - return {%filter}; + return {%filter};#copy? - test and fixme } sub show_patch_lines @@ -203,133 +1022,11 @@ sub show_patch_lines } } -sub get_gcno_file -{ - # Assumes test cases have been run, and "make rename_cov_data" has been executed - - my $file = shift; - my ($name, $path, $suffix) = fileparse($file, (".c", ".cpp", ".h")); - my $gcno_file = $repo->wc_path() . "/build/tizen/.cov/$name.gcno"; - - # Note, will translate headers to their source's object, which - # may miss execution code in the headers (e.g. inlines are usually - # not all used in the implementation, and require getting coverage - # from test cases. - - if( -f $gcno_file ) - { - my $gcno_st = stat($gcno_file); - my $fq_file = $repo->wc_path() . $file; - my $src_st = stat($fq_file); - if($gcno_st->ctime < $src_st->mtime) - { - print "WARNING: GCNO $gcno_file older than SRC $fq_file\n"; - $gcno_file=""; - } - - } - else - { - print("WARNING: No equivalent gcno file for $file\n"); - } - return $gcno_file; -} - -our %gcovfiles=(); -sub get_coverage -{ - my $file = shift; - my $filesref = shift; - print("get_coverage($file)\n") if $debug; - - my $gcno_file = get_gcno_file($file); - my @gcov_files = (); - my $gcovfile; - if( $gcno_file ) - { - print "Running gcov on $gcno_file:\n" if $debug; - open( my $fh, "gcov --preserve-paths $gcno_file |") || die "Can't run gcov:$!\n"; - while( <$fh> ) - { - print $_ if $debug>=3; - chomp; - if( m!'(.*\.gcov)'$! ) - { - my $coverage_file = $1; # File has / replaced with # and .. replaced with ^ - my $source_file = $coverage_file; - $source_file =~ s!\^!..!g; # Change ^ to .. - $source_file =~ s!\#!/!g; # change #'s to /s - $source_file =~ s!.gcov$!!; # Strip off .gcov suffix - - print "Matching $file against $source_file\n" if $debug >= 3; - # Only want the coverage files matching source file: - if(index( $source_file, $file ) > 0 ) - { - $gcovfile = $coverage_file; - # Some header files do not produce an equivalent gcov file so we shouldn't parse them - if(($source_file =~ /\.h$/) && (! -e $gcovfile)) - { - print "Omitting Header: $source_file\n" if $debug; - $gcovfile = "" - } - last; - } - } - } - close($fh); - - if($gcovfile) - { - if($gcovfiles{$gcovfile} == undef) - { - # Only parse a gcov file once - $gcovfiles{$gcovfile}->{"seen"}=1; - - print "Getting coverage data from $gcovfile\n" if $debug; - open( FH, "< $gcovfile" ) || die "Can't open $gcovfile for reading:$!\n"; - while() - { - my ($cov, $line, @code ) = split( /:/, $_ ); - $cov =~ s/^\s+//; # Strip leading space - $line =~ s/^\s+//; - my $code=join(":", @code); - if($cov =~ /\#/) - { - # There is no coverage data for these executable lines - $gcovfiles{$gcovfile}->{"uncovered"}->{$line}++; - $gcovfiles{$gcovfile}->{"src"}->{$line}=$code; - } - elsif( $cov ne "-" && looks_like_number($cov) && $cov > 0 ) - { - $gcovfiles{$gcovfile}->{"covered"}->{$line}=$cov; - $gcovfiles{$gcovfile}->{"src"}->{$line}=$code; - } - else - { - # All other lines are not executable. - $gcovfiles{$gcovfile}->{"src"}->{$line}=$code; - } - } - close( FH ); - } - $filesref->{$file}->{"coverage"} = $gcovfiles{$gcovfile}; # store hashref - } - else - { - # No gcov output - the gcno file produced no coverage of the source/header - # Probably means that there is no coverage for the file (with the given - # test case - there may be some somewhere, but for the sake of speed, don't - # check (yet). - } - } -} - -# Run the git diff command to get the patch, then check the coverage -# output for the patch. +# Run the git diff command to get the patch +# Output - see parse_diff sub run_diff { - #print "run_diff(" . join(" ", @_) . ")\n"; my ($fh, $c) = $repo->command_output_pipe(@_); our @patch=(); while(<$fh>) @@ -343,23 +1040,8 @@ sub run_diff # @patch has slurped diff for all files... my $filesref = parse_diff ( \@patch ); - show_patch_lines($filesref) if $debug; - - print "Checking coverage:\n" if $debug; + show_patch_lines($filesref) if $debug>1; - my $cwd=getcwd(); - chdir ".cov" || die "Can't find $cwd/.cov:$!\n"; - - for my $file (keys(%$filesref)) - { - my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$}); - next if($path !~ /^dali/); - if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h") - { - get_coverage($file, $filesref); - } - } - chdir $cwd; return $filesref; } @@ -378,35 +1060,45 @@ sub calc_patch_coverage_percentage my $uncovered_lines = 0; my $patchref = $filesref->{$file}->{"patch"}; - my $coverage_ref = $filesref->{$file}->{"coverage"}; - if( $coverage_ref ) + + my $abs_filename = File::Spec->rel2abs($file, $root); + my $sumcountref = $info_data{$abs_filename}->{"sum"}; + + if( $sumcountref ) { for my $patch (@$patchref) { for(my $i = 0; $i < $patch->[1]; $i++ ) { my $line = $i + $patch->[0]; - if($coverage_ref->{"covered"}->{$line}) - { - $covered_lines++; - $total_covered_lines++; - } - if($coverage_ref->{"uncovered"}->{$line}) + if(exists($sumcountref->{$line})) { - $uncovered_lines++; - $total_uncovered_lines++; + if( $sumcountref->{$line} > 0 ) + { + $covered_lines++; + $total_covered_lines++; + } + else + { + $uncovered_lines++; + $total_uncovered_lines++; + } } } } - $coverage_ref->{"covered_lines"} = $covered_lines; - $coverage_ref->{"uncovered_lines"} = $uncovered_lines; + $filesref->{$file}->{"covered_lines"} = $covered_lines; + $filesref->{$file}->{"uncovered_lines"} = $uncovered_lines; my $total = $covered_lines + $uncovered_lines; my $percent = 0; if($total > 0) { $percent = $covered_lines / $total; } - $coverage_ref->{"percent_covered"} = 100 * $percent; + $filesref->{$file}->{"percent_covered"} = 100 * $percent; + } + else + { + print "Can't find coverage data for $abs_filename\n"; } } my $total_exec = $total_covered_lines + $total_uncovered_lines; @@ -416,6 +1108,8 @@ sub calc_patch_coverage_percentage return [ $total_exec, $percent ]; } +# +# sub patch_output { my $filesref = shift; @@ -424,18 +1118,22 @@ sub patch_output my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$}); next if($path !~ /^dali/); - my $patchref = $filesref->{$file}->{"patch"}; - my $b_lines_ref = $filesref->{$file}->{"b_lines"}; - my $coverage_ref = $filesref->{$file}->{"coverage"}; + my $fileref = $filesref->{$file}; + my $patchref = $fileref->{"patch"}; + my $b_lines_ref = $fileref->{"b_lines"}; + + my $abs_filename = File::Spec->rel2abs($file, $root); + my $sumcountref = $info_data{$abs_filename}->{"sum"}; + print BOLD, "$file "; - if($coverage_ref) + if($fileref) { - if( $coverage_ref->{"covered_lines"} > 0 + if( $fileref->{"covered_lines"} > 0 || - $coverage_ref->{"uncovered_lines"} > 0 ) + $fileref->{"uncovered_lines"} > 0 ) { - print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET; + print GREEN, "Covered: " . $fileref->{"covered_lines"}, RED, " Uncovered: " . $fileref->{"uncovered_lines"}, RESET; } } else @@ -461,28 +1159,29 @@ sub patch_output my $line = $i + $patch->[0]; printf "%-6s ", $line; - if($coverage_ref) + if($sumcountref) { my $color; - if($coverage_ref->{"covered"}->{$line}) - { - $color=GREEN; - } - elsif($coverage_ref->{"uncovered"}->{$line}) + if(exists($sumcountref->{$line})) { - $color=BOLD . RED; + if($sumcountref->{$line} > 0) + { + $color=GREEN; + } + else + { + $color=BOLD . RED; + } } else { - $color=BLACK; + $color=CYAN; } - my $src=$coverage_ref->{"src"}->{$line}; - chomp($src); + my $src = $b_lines_ref->{$line}; print $color, "$src\n", RESET; } else { - # We don't have coverage data, so print it from the patch instead. my $src = $b_lines_ref->{$line}; print "$src\n"; } @@ -491,70 +1190,65 @@ sub patch_output } } - +# +# sub patch_html_output { my $filesref = shift; - my $html = HTML::Element->new('html'); - my $head = HTML::Element->new('head'); - my $title = HTML::Element->new('title'); - $title->push_content("Patch Coverage"); - $head->push_content($title, "\n"); - $html->push_content($head, "\n"); + open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n"; - my $body = HTML::Element->new('body'); - $body->attr('bgcolor', "white"); + my $OUTPUT_FH = select; + select $filehandle; + print < + + +Patch Coverage + + +EOH - foreach my $file (sort(keys(%$filesref))) + foreach my $file (keys(%$filesref)) { my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$}); next if($path !~ /^dali/); - my $patchref = $filesref->{$file}->{"patch"}; - my $b_lines_ref = $filesref->{$file}->{"b_lines"}; - my $coverage_ref = $filesref->{$file}->{"coverage"}; - - my $header = HTML::Element->new('h2'); - $header->push_content($file); - $body->push_content($header); - $body->push_content("\n"); - if($coverage_ref) + my $fileref = $filesref->{$file}; + my $patchref = $fileref->{"patch"}; + my $b_lines_ref = $fileref->{"b_lines"}; + + my $abs_filename = File::Spec->rel2abs($file, $root); + my $sumcountref = $info_data{$abs_filename}->{"sum"}; + + print "

$file

\n"; + + if($fileref) { - if( $coverage_ref->{"covered_lines"} > 0 + if( $fileref->{"covered_lines"} > 0 || - $coverage_ref->{"uncovered_lines"} > 0 ) + $fileref->{"uncovered_lines"} > 0 ) { - my $para = HTML::Element->new('p'); - my $covered = HTML::Element->new('span'); - $covered->attr('style', "color:green;"); - $covered->push_content("Covered: " . $coverage_ref->{"covered_lines"} ); - $para->push_content($covered); - - my $para2 = HTML::Element->new('p'); - my $uncovered = HTML::Element->new('span'); - $uncovered->attr('style', "color:red;"); - $uncovered->push_content("Uncovered: " . $coverage_ref->{"uncovered_lines"} ); - $para2->push_content($uncovered); - $body->push_content($para, $para2); - } - else - { - #print "coverage ref exists for $file:\n" . Data::Dumper::Dumper($coverage_ref) . "\n"; + print "

Covered: " . + $fileref->{"covered_lines"} . "

" . + "

Uncovered: " . + $fileref->{"uncovered_lines"} . "

"; } } else { - my $para = HTML::Element->new('p'); - my $span = HTML::Element->new('span'); + print "

"; + my $span=0; if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h") { - $span->attr('style', "color:red;"); + print ""; + $span=1; } - $span->push_content("No coverage found"); - $para->push_content($span); - $body->push_content($para); + print "No coverage found"; + print "" if $span; } + print "

"; for my $patch (@$patchref) { @@ -563,71 +1257,54 @@ sub patch_html_output { $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1); } + print "

" . $hunkstr . "

"; - my $para = HTML::Element->new('p'); - my $span = HTML::Element->new('span'); - $span->attr('style', "font-weight:bold;"); - $span->push_content($hunkstr); - $para->push_content($span); - $body->push_content($para); - - my $codeHunk = HTML::Element->new('pre'); + print "
";
             for(my $i = 0; $i < $patch->[1]; $i++ )
             {
                 my $line = $i + $patch->[0];
                 my $num_line_digits=log($line)/log(10);
                 for $i (0..(6-$num_line_digits-1))
                 {
-                    $codeHunk->push_content(" ");
+                    print " ";
                 }
+                print "$line  ";
 
-                $codeHunk->push_content("$line  ");
-
-                my $srcLine = HTML::Element->new('span');
-                if($coverage_ref)
+                if($sumcountref)
                 {
                     my $color;
-
-                    if($coverage_ref->{"covered"}->{$line})
-                    {
-                        $srcLine->attr('style', "color:green;");
-                    }
-                    elsif($coverage_ref->{"uncovered"}->{$line})
+                    if(exists($sumcountref->{$line}))
                     {
-                        $srcLine->attr('style', "color:red;font-weight:bold;");
+                        if($sumcountref->{$line} > 0)
+                        {
+                            print("");
+                        }
+                        else
+                        {
+                            print("");
+                        }
                     }
                     else
                     {
-                        $srcLine->attr('style', "color:black;font-weight:normal;");
+                        print("");
                     }
-                    my $src=$coverage_ref->{"src"}->{$line};
+                    my $src=$b_lines_ref->{$line};
                     chomp($src);
-                    $srcLine->push_content($src);
+                    print "$src\n";
                 }
                 else
                 {
-                    # We don't have coverage data, so print it from the patch instead.
                     my $src = $b_lines_ref->{$line};
-                    $srcLine->attr('style', "color:black;font-weight:normal;");
-                    $srcLine->push_content($src);
+                    print "$src\n";
                 }
-                $codeHunk->push_content($srcLine, "\n");
             }
-            $body->push_content($codeHunk, "\n");
+            print "<\pre>\n";
         }
     }
-    $body->push_content(HTML::Element->new('hr'));
-    $html->push_content($body, "\n");
-
-    open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
 
-    print $filehandle <
-EOH
-;
-    print $filehandle $html->as_HTML();
+    print $filehandle "
\n\n\n"; close $filehandle; + select $OUTPUT_FH; } @@ -635,15 +1312,38 @@ EOH ## MAIN ## ################################################################################ -my $cwd = getcwd(); -chdir $repo->wc_path(); -chdir "build/tizen"; -`make rename_cov_data`; +my $cwd = getcwd(); # expect this to be automated-tests folder -my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color'); +# execute coverage.sh, generating build/tizen/dali.info from lib, and +# *.dir/dali.info. Don't generate html +`coverage.sh -n`; +chdir ".."; +$root = getcwd(); + +our %info_data; # Hash of all data from .info files +my @info_files = split(/\n/, `find . -name dali.info`); +my %new_info; +# Read in all specified .info files +foreach (@info_files) +{ + %new_info = %{read_info_file($_)}; + + # Combine %new_info with %info_data + %info_data = %{combine_info_files(\%info_data, \%new_info)}; +} + + +# Generate git diff command +my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color'); my $status = $repo->command("status", "-s"); -if( $status eq "" && !scalar(@ARGV)) + +if(scalar(@ARGV)) # REMOVE ME +{ + # REMOVE ME - temp to get past modifying this script in place. + push @cmd, @ARGV; +} +elsif( $status eq "" && !scalar(@ARGV)) { # There are no changes in the index or working tree, and # no diff arguments to append. Use the last patch instead. @@ -674,6 +1374,8 @@ else } push @cmd, @ARGV; + +# Execute diff & coverage from root directory my $filesref = run_diff(@cmd); chdir $cwd; @@ -693,6 +1395,9 @@ if( $filecount == 0 ) exit 0; # Exit with no error. } +#print_simplified_info() if $debug; +#exit 0; + my $percentref = calc_patch_coverage_percentage($filesref); if($percentref->[0] == 0) { diff --git a/automated-tests/src/dali-toolkit/utc-Dali-ImageView.cpp b/automated-tests/src/dali-toolkit/utc-Dali-ImageView.cpp index 7b0b54d..f31da7c 100644 --- a/automated-tests/src/dali-toolkit/utc-Dali-ImageView.cpp +++ b/automated-tests/src/dali-toolkit/utc-Dali-ImageView.cpp @@ -2749,11 +2749,20 @@ void OnResourceReadySignal( Control control ) { gResourceReadySignalCounter++; - if( gResourceReadySignalCounter == 1 ) + if(control.GetVisualResourceStatus(ImageView::Property::IMAGE) == Visual::ResourceStatus::READY) { - // Set image twice - ImageView::DownCast( control ).SetImage( gImage_34_RGBA ); - ImageView::DownCast( control ).SetImage( gImage_34_RGBA ); + if( gResourceReadySignalCounter == 1 ) + { + // Set image twice + // It makes the first new visual be deleted immediately + ImageView::DownCast( control ).SetImage( gImage_34_RGBA ); + ImageView::DownCast( control ).SetImage( gImage_34_RGBA ); + } + } + else if(control.GetVisualResourceStatus(ImageView::Property::IMAGE) == Visual::ResourceStatus::FAILED) + { + // Make the resource ready immediately + control[ImageView::Property::IMAGE] = TEST_RESOURCE_DIR "/svg1.svg"; } } @@ -2781,5 +2790,22 @@ int UtcDaliImageViewSetImageOnResourceReadySignal(void) DALI_TEST_EQUALS( imageView.IsResourceReady(), true, TEST_LOCATION ); + // Reset count + gResourceReadySignalCounter = 0; + + imageView[ImageView::Property::IMAGE] = "invalid.jpg"; + + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1 ), true, TEST_LOCATION ); + + application.SendNotification(); + application.Render(); + + // Run idle callback + application.RunIdles(); + + DALI_TEST_EQUALS( gResourceReadySignalCounter, 2, TEST_LOCATION ); + + DALI_TEST_EQUALS( imageView.IsResourceReady(), true, TEST_LOCATION ); + END_TEST; } diff --git a/dali-toolkit/internal/controls/control/control-data-impl.cpp b/dali-toolkit/internal/controls/control/control-data-impl.cpp index 8a2dfd7..368ab0a 100755 --- a/dali-toolkit/internal/controls/control/control-data-impl.cpp +++ b/dali-toolkit/internal/controls/control/control-data-impl.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -322,7 +323,7 @@ Control::Impl::Impl( Control& controlImpl ) mDownFocusableActorId( -1 ), mStyleName(""), mBackgroundColor(Color::TRANSPARENT), - mStartingPinchScale( NULL ), + mStartingPinchScale(nullptr), mMargin( 0, 0, 0, 0 ), mPadding( 0, 0, 0, 0 ), mKeyEventSignal(), @@ -336,9 +337,12 @@ Control::Impl::Impl( Control& controlImpl ) mLongPressGestureDetector(), mTooltip( NULL ), mInputMethodContext(), + mIdleCallback(nullptr), mFlags( Control::ControlBehaviour( CONTROL_BEHAVIOUR_DEFAULT ) ), mIsKeyboardNavigationSupported( false ), - mIsKeyboardFocusGroup( false ) + mIsKeyboardFocusGroup( false ), + mIsEmittingResourceReadySignal(false), + mNeedToEmitResourceReady(false) { } @@ -346,6 +350,12 @@ Control::Impl::~Impl() { // All gesture detectors will be destroyed so no need to disconnect. delete mStartingPinchScale; + + if(mIdleCallback && Adaptor::IsAvailable()) + { + // Removes the callback from the callback manager in case the control is destroyed before the callback is executed. + Adaptor::Get().RemoveIdle(mIdleCallback); + } } Control::Impl& Control::Impl::Get( Internal::Control& internalControl ) @@ -670,8 +680,10 @@ void Control::Impl::ResourceReady( Visual::Base& object) // Emit signal if all enabled visuals registered by the control are ready. if( IsResourceReady() ) { - Dali::Toolkit::Control handle( mControlImpl.GetOwner() ); - mResourceReadySignal.Emit( handle ); + // Reset the flag + mNeedToEmitResourceReady = false; + + EmitResourceReadySignal(); } } @@ -1448,6 +1460,58 @@ void Control::Impl::ClearShadow() mControlImpl.RelayoutRequest(); } +void Control::Impl::EmitResourceReadySignal() +{ + if(!mIsEmittingResourceReadySignal) + { + // Guard against calls to emit the signal during the callback + mIsEmittingResourceReadySignal = true; + + // If the signal handler changes visual, it may become ready during this call & therefore this method will + // get called again recursively. If so, mNeedToEmitResourceReady is set below, and we act on it after that secondary + // invocation has completed by notifying in an Idle callback to prevent further recursion. + Dali::Toolkit::Control handle(mControlImpl.GetOwner()); + mResourceReadySignal.Emit(handle); + + if(mNeedToEmitResourceReady) + { + // Add idler to emit the signal again + if(!mIdleCallback) + { + // The callback manager takes the ownership of the callback object. + mIdleCallback = MakeCallback( this, &Control::Impl::OnIdleCallback); + Adaptor::Get().AddIdle(mIdleCallback, false); + } + } + + mIsEmittingResourceReadySignal = false; + } + else + { + mNeedToEmitResourceReady = true; + } +} + +void Control::Impl::OnIdleCallback() +{ + if(mNeedToEmitResourceReady) + { + // Reset the flag + mNeedToEmitResourceReady = false; + + // A visual is ready so control may need relayouting if staged + if(mControlImpl.Self().GetProperty(Actor::Property::CONNECTED_TO_SCENE)) + { + mControlImpl.RelayoutRequest(); + } + + EmitResourceReadySignal(); + } + + // Set the pointer to null as the callback manager deletes the callback after execute it. + mIdleCallback = nullptr; +} + } // namespace Internal } // namespace Toolkit diff --git a/dali-toolkit/internal/controls/control/control-data-impl.h b/dali-toolkit/internal/controls/control/control-data-impl.h index 43cab69..e042113 100755 --- a/dali-toolkit/internal/controls/control/control-data-impl.h +++ b/dali-toolkit/internal/controls/control/control-data-impl.h @@ -386,6 +386,16 @@ private: */ void RegisterVisual( Property::Index index, Toolkit::Visual::Base& visual, VisualState::Type enabled, DepthIndexValue::Type depthIndexValueSet, int depthIndex = 0 ); + /** + * @brief Emits the resource ready signal. + */ + void EmitResourceReadySignal(); + + /** + * @brief Callbacks called on idle. + */ + void OnIdleCallback(); + public: Control& mControlImpl; @@ -419,10 +429,13 @@ public: TooltipPtr mTooltip; InputMethodContext mInputMethodContext; + CallbackBase* mIdleCallback; ///< The idle callback to emit the resource ready signal. ControlBehaviour mFlags : CONTROL_BEHAVIOUR_FLAG_COUNT; ///< Flags passed in from constructor. bool mIsKeyboardNavigationSupported :1; ///< Stores whether keyboard navigation is supported by the control. bool mIsKeyboardFocusGroup :1; ///< Stores whether the control is a focus group. + bool mIsEmittingResourceReadySignal :1; ///< True during ResourceReady(). + bool mNeedToEmitResourceReady :1; ///< True if need to emit the resource ready signal again. RegisteredVisualContainer mRemoveVisuals; ///< List of visuals that are being replaced by another visual once ready diff --git a/dali-toolkit/internal/controls/text-controls/text-label-impl.cpp b/dali-toolkit/internal/controls/text-controls/text-label-impl.cpp index 1cdb0f8..d1c1b1a 100755 --- a/dali-toolkit/internal/controls/text-controls/text-label-impl.cpp +++ b/dali-toolkit/internal/controls/text-controls/text-label-impl.cpp @@ -62,33 +62,29 @@ namespace Internal namespace { - const unsigned int DEFAULT_RENDERING_BACKEND = Dali::Toolkit::DevelText::DEFAULT_RENDERING_BACKEND; - - /** - * @brief How the text visual should be aligned vertically inside the control. - * - * 0.0f aligns the text to the top, 0.5f aligns the text to the center, 1.0f aligns the text to the bottom. - * The alignment depends on the alignment value of the text label (Use Text::VerticalAlignment enumerations). - */ - const float VERTICAL_ALIGNMENT_TABLE[ Text::VerticalAlignment::BOTTOM + 1 ] = - { - 0.0f, // VerticalAlignment::TOP - 0.5f, // VerticalAlignment::CENTER - 1.0f // VerticalAlignment::BOTTOM - }; - - const std::string TEXT_FIT_ENABLE_KEY( "enable" ); - const std::string TEXT_FIT_MIN_SIZE_KEY( "minSize" ); - const std::string TEXT_FIT_MAX_SIZE_KEY( "maxSize" ); - const std::string TEXT_FIT_STEP_SIZE_KEY( "stepSize" ); - const std::string TEXT_FIT_FONT_SIZE_TYPE_KEY( "fontSizeType" ); -} +const unsigned int DEFAULT_RENDERING_BACKEND = Dali::Toolkit::DevelText::DEFAULT_RENDERING_BACKEND; -namespace +/** + * @brief How the text visual should be aligned vertically inside the control. + * + * 0.0f aligns the text to the top, 0.5f aligns the text to the center, 1.0f aligns the text to the bottom. + * The alignment depends on the alignment value of the text label (Use Text::VerticalAlignment enumerations). + */ +const float VERTICAL_ALIGNMENT_TABLE[ Text::VerticalAlignment::BOTTOM + 1 ] = { + 0.0f, // VerticalAlignment::TOP + 0.5f, // VerticalAlignment::CENTER + 1.0f // VerticalAlignment::BOTTOM +}; + +const std::string TEXT_FIT_ENABLE_KEY( "enable" ); +const std::string TEXT_FIT_MIN_SIZE_KEY( "minSize" ); +const std::string TEXT_FIT_MAX_SIZE_KEY( "maxSize" ); +const std::string TEXT_FIT_STEP_SIZE_KEY( "stepSize" ); +const std::string TEXT_FIT_FONT_SIZE_TYPE_KEY( "fontSizeType" ); #if defined ( DEBUG_ENABLED ) - Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); +Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); #endif const Scripting::StringEnum AUTO_SCROLL_STOP_MODE_TABLE[] = @@ -145,6 +141,73 @@ DALI_ANIMATABLE_PROPERTY_COMPONENT_REGISTRATION( Toolkit, TextLabel, "textCol DALI_ANIMATABLE_PROPERTY_COMPONENT_REGISTRATION( Toolkit, TextLabel, "textColorAlpha", TEXT_COLOR_ALPHA, TEXT_COLOR, 3 ) DALI_TYPE_REGISTRATION_END() +/// Parses the property map for the TEXT_FIT property +void ParseTextFitProperty(Text::ControllerPtr& controller, const Property::Map* propertiesMap) +{ + if ( propertiesMap && !propertiesMap->Empty() ) + { + bool enabled = false; + float minSize = 0.f; + float maxSize = 0.f; + float stepSize = 0.f; + bool isMinSizeSet = false, isMaxSizeSet = false, isStepSizeSet = false; + Controller::FontSizeType type = Controller::FontSizeType::POINT_SIZE; + + const unsigned int numberOfItems = propertiesMap->Count(); + + // Parses and applies + for( unsigned int index = 0u; index < numberOfItems; ++index ) + { + const KeyValuePair& valueGet = propertiesMap->GetKeyValue( index ); + + if( ( Controller::TextFitInfo::Property::TEXT_FIT_ENABLE == valueGet.first.indexKey ) || ( TEXT_FIT_ENABLE_KEY == valueGet.first.stringKey ) ) + { + /// Enable key. + enabled = valueGet.second.Get< bool >(); + } + else if( ( Controller::TextFitInfo::Property::TEXT_FIT_MIN_SIZE == valueGet.first.indexKey ) || ( TEXT_FIT_MIN_SIZE_KEY == valueGet.first.stringKey ) ) + { + /// min size. + minSize = valueGet.second.Get< float >(); + isMinSizeSet = true; + } + else if( ( Controller::TextFitInfo::Property::TEXT_FIT_MAX_SIZE == valueGet.first.indexKey ) || ( TEXT_FIT_MAX_SIZE_KEY == valueGet.first.stringKey ) ) + { + /// max size. + maxSize = valueGet.second.Get< float >(); + isMaxSizeSet = true; + } + else if( ( Controller::TextFitInfo::Property::TEXT_FIT_STEP_SIZE == valueGet.first.indexKey ) || ( TEXT_FIT_STEP_SIZE_KEY == valueGet.first.stringKey ) ) + { + /// step size. + stepSize = valueGet.second.Get< float >(); + isStepSizeSet = true; + } + else if( ( Controller::TextFitInfo::Property::TEXT_FIT_FONT_SIZE_TYPE == valueGet.first.indexKey ) || ( TEXT_FIT_FONT_SIZE_TYPE_KEY == valueGet.first.stringKey ) ) + { + if( "pixelSize" == valueGet.second.Get< std::string >() ) + { + type = Controller::FontSizeType::PIXEL_SIZE; + } + } + } + + controller->SetTextFitEnabled( enabled ); + if( isMinSizeSet ) + { + controller->SetTextFitMinSize( minSize, type ); + } + if( isMaxSizeSet ) + { + controller->SetTextFitMaxSize( maxSize, type ); + } + if( isStepSizeSet ) + { + controller->SetTextFitStepSize( stepSize, type ); + } + } +} + } // namespace Toolkit::TextLabel TextLabel::New() @@ -275,101 +338,61 @@ void TextLabel::SetProperty( BaseObject* object, Property::Index index, const Pr } case Toolkit::TextLabel::Property::AUTO_SCROLL_STOP_MODE: { - if( !impl.mTextScroller ) - { - impl.mTextScroller = Text::TextScroller::New( impl ); - } - Toolkit::TextLabel::AutoScrollStopMode::Type stopMode = impl.mTextScroller->GetStopMode(); + Text::TextScrollerPtr textScroller = impl.GetTextScroller(); + Toolkit::TextLabel::AutoScrollStopMode::Type stopMode = textScroller->GetStopMode(); if( Scripting::GetEnumerationProperty< Toolkit::TextLabel::AutoScrollStopMode::Type >( value, - AUTO_SCROLL_STOP_MODE_TABLE, - AUTO_SCROLL_STOP_MODE_TABLE_COUNT, - stopMode ) ) + AUTO_SCROLL_STOP_MODE_TABLE, + AUTO_SCROLL_STOP_MODE_TABLE_COUNT, + stopMode ) ) { - impl.mTextScroller->SetStopMode( stopMode ); + textScroller->SetStopMode( stopMode ); } break; } case Toolkit::TextLabel::Property::AUTO_SCROLL_SPEED: { - if( !impl.mTextScroller ) - { - impl.mTextScroller = Text::TextScroller::New( impl ); - } - impl.mTextScroller->SetSpeed( value.Get() ); + impl.GetTextScroller()->SetSpeed( value.Get() ); break; } case Toolkit::TextLabel::Property::AUTO_SCROLL_LOOP_COUNT: { - if( !impl.mTextScroller ) - { - impl.mTextScroller = Text::TextScroller::New( impl ); - } - impl.mTextScroller->SetLoopCount( value.Get() ); + impl.GetTextScroller()->SetLoopCount( value.Get() ); break; } case Toolkit::TextLabel::Property::AUTO_SCROLL_LOOP_DELAY: { - if( !impl.mTextScroller ) - { - impl.mTextScroller = Text::TextScroller::New( impl ); - } - impl.mTextScroller->SetLoopDelay( value.Get() ); + impl.GetTextScroller()->SetLoopDelay( value.Get() ); break; } case Toolkit::TextLabel::Property::AUTO_SCROLL_GAP: { - if( !impl.mTextScroller ) - { - impl.mTextScroller = Text::TextScroller::New( impl ); - } - impl.mTextScroller->SetGap( value.Get() ); + impl.GetTextScroller()->SetGap( value.Get() ); break; } case Toolkit::TextLabel::Property::LINE_SPACING: { const float lineSpacing = value.Get(); - - // Don't trigger anything if the line spacing didn't change - if( impl.mController->SetDefaultLineSpacing( lineSpacing ) ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = impl.mController->SetDefaultLineSpacing( lineSpacing ) || impl.mTextUpdateNeeded; break; } case Toolkit::TextLabel::Property::UNDERLINE: { - const bool update = SetUnderlineProperties( impl.mController, value, Text::EffectStyle::DEFAULT ); - if( update ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = SetUnderlineProperties( impl.mController, value, Text::EffectStyle::DEFAULT ) || impl.mTextUpdateNeeded; break; } case Toolkit::TextLabel::Property::SHADOW: { - const bool update = SetShadowProperties( impl.mController, value, Text::EffectStyle::DEFAULT ); - if( update ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = SetShadowProperties( impl.mController, value, Text::EffectStyle::DEFAULT ) || impl.mTextUpdateNeeded; break; } case Toolkit::TextLabel::Property::EMBOSS: { - const bool update = SetEmbossProperties( impl.mController, value, Text::EffectStyle::DEFAULT ); - if( update ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = SetEmbossProperties( impl.mController, value, Text::EffectStyle::DEFAULT ) || impl.mTextUpdateNeeded; break; } case Toolkit::TextLabel::Property::OUTLINE: { - const bool update = SetOutlineProperties( impl.mController, value, Text::EffectStyle::DEFAULT ); - if( update ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = SetOutlineProperties( impl.mController, value, Text::EffectStyle::DEFAULT ) || impl.mTextUpdateNeeded; break; } case Toolkit::TextLabel::Property::PIXEL_SIZE: @@ -419,11 +442,7 @@ void TextLabel::SetProperty( BaseObject* object, Property::Index index, const Pr } case Toolkit::DevelTextLabel::Property::BACKGROUND: { - const bool update = SetBackgroundProperties( impl.mController, value, Text::EffectStyle::DEFAULT ); - if( update ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = SetBackgroundProperties( impl.mController, value, Text::EffectStyle::DEFAULT ) || impl.mTextUpdateNeeded; break; } case Toolkit::DevelTextLabel::Property::IGNORE_SPACES_AFTER_TEXT: @@ -438,80 +457,13 @@ void TextLabel::SetProperty( BaseObject* object, Property::Index index, const Pr } case Toolkit::DevelTextLabel::Property::TEXT_FIT: { - const Property::Map& propertiesMap = value.Get(); - - bool enabled = false; - float minSize = 0.f; - float maxSize = 0.f; - float stepSize = 0.f; - bool isMinSizeSet = false, isMaxSizeSet = false, isStepSizeSet = false; - Controller::FontSizeType type = Controller::FontSizeType::POINT_SIZE; - - if ( !propertiesMap.Empty() ) - { - const unsigned int numberOfItems = propertiesMap.Count(); - - // Parses and applies - for( unsigned int index = 0u; index < numberOfItems; ++index ) - { - const KeyValuePair& valueGet = propertiesMap.GetKeyValue( index ); - - if( ( Controller::TextFitInfo::Property::TEXT_FIT_ENABLE == valueGet.first.indexKey ) || ( TEXT_FIT_ENABLE_KEY == valueGet.first.stringKey ) ) - { - /// Enable key. - enabled = valueGet.second.Get< bool >(); - } - else if( ( Controller::TextFitInfo::Property::TEXT_FIT_MIN_SIZE == valueGet.first.indexKey ) || ( TEXT_FIT_MIN_SIZE_KEY == valueGet.first.stringKey ) ) - { - /// min size. - minSize = valueGet.second.Get< float >(); - isMinSizeSet = true; - } - else if( ( Controller::TextFitInfo::Property::TEXT_FIT_MAX_SIZE == valueGet.first.indexKey ) || ( TEXT_FIT_MAX_SIZE_KEY == valueGet.first.stringKey ) ) - { - /// max size. - maxSize = valueGet.second.Get< float >(); - isMaxSizeSet = true; - } - else if( ( Controller::TextFitInfo::Property::TEXT_FIT_STEP_SIZE == valueGet.first.indexKey ) || ( TEXT_FIT_STEP_SIZE_KEY == valueGet.first.stringKey ) ) - { - /// step size. - stepSize = valueGet.second.Get< float >(); - isStepSizeSet = true; - } - else if( ( Controller::TextFitInfo::Property::TEXT_FIT_FONT_SIZE_TYPE == valueGet.first.indexKey ) || ( TEXT_FIT_FONT_SIZE_TYPE_KEY == valueGet.first.stringKey ) ) - { - if( "pixelSize" == valueGet.second.Get< std::string >() ) - { - type = Controller::FontSizeType::PIXEL_SIZE; - } - } - } - - impl.mController->SetTextFitEnabled( enabled ); - if( isMinSizeSet ) - { - impl.mController->SetTextFitMinSize( minSize, type ); - } - if( isMaxSizeSet ) - { - impl.mController->SetTextFitMaxSize( maxSize, type ); - } - if( isStepSizeSet ) - { - impl.mController->SetTextFitStepSize( stepSize, type ); - } - } + ParseTextFitProperty(impl.mController, value.GetMap()); break; } case Toolkit::DevelTextLabel::Property::MIN_LINE_SIZE: { const float lineSize = value.Get(); - - if( impl.mController->SetDefaultLineSize( lineSize ) ) - { - impl.mTextUpdateNeeded = true; - } + impl.mTextUpdateNeeded = impl.mController->SetDefaultLineSize( lineSize ) || impl.mTextUpdateNeeded; break; } } diff --git a/dali-toolkit/internal/controls/text-controls/text-label-impl.h b/dali-toolkit/internal/controls/text-controls/text-label-impl.h index bdbdbbc..a863c9c 100644 --- a/dali-toolkit/internal/controls/text-controls/text-label-impl.h +++ b/dali-toolkit/internal/controls/text-controls/text-label-impl.h @@ -2,7 +2,7 @@ #define DALI_TOOLKIT_INTERNAL_TEXT_LABEL_H /* - * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * Copyright (c) 2020 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -142,6 +142,19 @@ private: */ void SetUpAutoScrolling(); + /** + * Creates a text-scroller if one has not been created. + * @return The text scroller. + */ + Text::TextScrollerPtr GetTextScroller() + { + if( !mTextScroller ) + { + mTextScroller = Text::TextScroller::New( *this ); + } + return mTextScroller; + } + private: // Data Text::ControllerPtr mController; diff --git a/dali-toolkit/internal/file.list b/dali-toolkit/internal/file.list index 0d6b781..72a5626 100644 --- a/dali-toolkit/internal/file.list +++ b/dali-toolkit/internal/file.list @@ -142,6 +142,7 @@ SET( toolkit_src_files ${toolkit_src_dir}/text/text-controller.cpp ${toolkit_src_dir}/text/text-controller-event-handler.cpp ${toolkit_src_dir}/text/text-controller-impl.cpp + ${toolkit_src_dir}/text/text-controller-impl-event-handler.cpp ${toolkit_src_dir}/text/text-controller-input-font-handler.cpp ${toolkit_src_dir}/text/text-controller-placeholder-handler.cpp ${toolkit_src_dir}/text/text-effects-style.cpp diff --git a/dali-toolkit/internal/text/rendering/text-typesetter.cpp b/dali-toolkit/internal/text/rendering/text-typesetter.cpp index 0256a43..05b4a67 100755 --- a/dali-toolkit/internal/text/rendering/text-typesetter.cpp +++ b/dali-toolkit/internal/text/rendering/text-typesetter.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Samsung Electronics Co., Ltd. + * Copyright (c) 2020 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -278,6 +278,149 @@ bool IsGlyphUnderlined( GlyphIndex index, return false; } +/// Helper method to fetch the underline metrics for the specified font glyph +void FetchFontUnderlineMetrics( + TextAbstraction::FontClient& fontClient, + const GlyphInfo* const glyphInfo, + float& currentUnderlinePosition, + const float underlineHeight, + float& currentUnderlineThickness, + float& maxUnderlineThickness, + FontId& lastUnderlinedFontId) +{ + FontMetrics fontMetrics; + fontClient.GetFontMetrics( glyphInfo->fontId, fontMetrics ); + currentUnderlinePosition = ceil( fabsf( fontMetrics.underlinePosition ) ); + const float descender = ceil( fabsf( fontMetrics.descender ) ); + + if( fabsf( underlineHeight ) < Math::MACHINE_EPSILON_1000 ) + { + currentUnderlineThickness = fontMetrics.underlineThickness; + + // Ensure underline will be at least a pixel high + if ( currentUnderlineThickness < 1.0f ) + { + currentUnderlineThickness = 1.0f; + } + else + { + currentUnderlineThickness = ceil( currentUnderlineThickness ); + } + } + + // The underline thickness should be the max underline thickness of all glyphs of the line. + if ( currentUnderlineThickness > maxUnderlineThickness ) + { + maxUnderlineThickness = currentUnderlineThickness; + } + + // Clamp the underline position at the font descender and check for ( as EFL describes it ) a broken font + if( currentUnderlinePosition > descender ) + { + currentUnderlinePosition = descender; + } + + if( fabsf( currentUnderlinePosition ) < Math::MACHINE_EPSILON_1000 ) + { + // Move offset down by one ( EFL behavior ) + currentUnderlinePosition = 1.0f; + } + + lastUnderlinedFontId = glyphInfo->fontId; +} + +/// Draws the specified color to the pixel buffer +void WriteColorToPixelBuffer( + GlyphData& glyphData, + uint32_t* bitmapBuffer, + const Vector4& color, + const unsigned int x, + const unsigned int y) +{ + // Always RGBA image for text with styles + uint32_t pixel = *( bitmapBuffer + y * glyphData.width + x ); + uint8_t* pixelBuffer = reinterpret_cast( &pixel ); + + // Write the color to the pixel buffer + uint8_t colorAlpha = static_cast< uint8_t >( color.a * 255.f ); + *( pixelBuffer + 3u ) = colorAlpha; + *( pixelBuffer + 2u ) = static_cast< uint8_t >( color.b * colorAlpha ); + *( pixelBuffer + 1u ) = static_cast< uint8_t >( color.g * colorAlpha ); + *( pixelBuffer ) = static_cast< uint8_t >( color.r * colorAlpha ); + + *( bitmapBuffer + y * glyphData.width + x ) = pixel; +} + +/// Draws the specified underline color to the buffer +void DrawUnderline( + const Vector4& underlineColor, + const unsigned int bufferWidth, + const unsigned int bufferHeight, + GlyphData& glyphData, + const float baseline, + const float currentUnderlinePosition, + const float maxUnderlineThickness, + const float lineExtentLeft, + const float lineExtentRight) +{ + int underlineYOffset = glyphData.verticalOffset + baseline + currentUnderlinePosition; + uint32_t* bitmapBuffer = reinterpret_cast< uint32_t* >( glyphData.bitmapBuffer.GetBuffer() ); + + for( unsigned int y = underlineYOffset; y < underlineYOffset + maxUnderlineThickness; y++ ) + { + if( y > bufferHeight - 1 ) + { + // Do not write out of bounds. + break; + } + + for( unsigned int x = glyphData.horizontalOffset + lineExtentLeft; x <= glyphData.horizontalOffset + lineExtentRight; x++ ) + { + if( x > bufferWidth - 1 ) + { + // Do not write out of bounds. + break; + } + + WriteColorToPixelBuffer(glyphData, bitmapBuffer, underlineColor, x, y); + } + } +} + +/// Draws the background color to the buffer +void DrawBackgroundColor( + Vector4 backgroundColor, + const unsigned int bufferWidth, + const unsigned int bufferHeight, + GlyphData& glyphData, + const float baseline, + const LineRun& line, + const float lineExtentLeft, + const float lineExtentRight) +{ + uint32_t* bitmapBuffer = reinterpret_cast< uint32_t* >( glyphData.bitmapBuffer.GetBuffer() ); + + for( int y = glyphData.verticalOffset + baseline - line.ascender; y < glyphData.verticalOffset + baseline - line.descender; y++ ) + { + if( ( y < 0 ) || ( y > static_cast(bufferHeight - 1) ) ) + { + // Do not write out of bounds. + continue; + } + + for( int x = glyphData.horizontalOffset + lineExtentLeft; x <= glyphData.horizontalOffset + lineExtentRight; x++ ) + { + if( ( x < 0 ) || ( x > static_cast(bufferWidth - 1) ) ) + { + // Do not write out of bounds. + continue; + } + + WriteColorToPixelBuffer(glyphData, bitmapBuffer, backgroundColor, x, y); + } + } +} + } // namespace TypesetterPtr Typesetter::New( const ModelInterface* const model ) @@ -593,45 +736,7 @@ Devel::PixelBuffer Typesetter::CreateImageBuffer( const unsigned int bufferWidth if( underlineGlyph && ( glyphInfo->fontId != lastUnderlinedFontId ) ) { // We need to fetch fresh font underline metrics - FontMetrics fontMetrics; - fontClient.GetFontMetrics( glyphInfo->fontId, fontMetrics ); - currentUnderlinePosition = ceil( fabsf( fontMetrics.underlinePosition ) ); - const float descender = ceil( fabsf( fontMetrics.descender ) ); - - if( fabsf( underlineHeight ) < Math::MACHINE_EPSILON_1000 ) - { - currentUnderlineThickness = fontMetrics.underlineThickness; - - // Ensure underline will be at least a pixel high - if ( currentUnderlineThickness < 1.0f ) - { - currentUnderlineThickness = 1.0f; - } - else - { - currentUnderlineThickness = ceil( currentUnderlineThickness ); - } - } - - // The underline thickness should be the max underline thickness of all glyphs of the line. - if ( currentUnderlineThickness > maxUnderlineThickness ) - { - maxUnderlineThickness = currentUnderlineThickness; - } - - // Clamp the underline position at the font descender and check for ( as EFL describes it ) a broken font - if( currentUnderlinePosition > descender ) - { - currentUnderlinePosition = descender; - } - - if( fabsf( currentUnderlinePosition ) < Math::MACHINE_EPSILON_1000 ) - { - // Move offset down by one ( EFL behavior ) - currentUnderlinePosition = 1.0f; - } - - lastUnderlinedFontId = glyphInfo->fontId; + FetchFontUnderlineMetrics(fontClient, glyphInfo, currentUnderlinePosition, underlineHeight, currentUnderlineThickness, maxUnderlineThickness, lastUnderlinedFontId); } // underline // Retrieves the glyph's position. @@ -728,77 +833,13 @@ Devel::PixelBuffer Typesetter::CreateImageBuffer( const unsigned int bufferWidth // Draw the underline from the leftmost glyph to the rightmost glyph if ( thereAreUnderlinedGlyphs && style == Typesetter::STYLE_UNDERLINE ) { - int underlineYOffset = glyphData.verticalOffset + baseline + currentUnderlinePosition; - - for( unsigned int y = underlineYOffset; y < underlineYOffset + maxUnderlineThickness; y++ ) - { - if( y > bufferHeight - 1 ) - { - // Do not write out of bounds. - break; - } - - for( unsigned int x = glyphData.horizontalOffset + lineExtentLeft; x <= glyphData.horizontalOffset + lineExtentRight; x++ ) - { - if( x > bufferWidth - 1 ) - { - // Do not write out of bounds. - break; - } - - // Always RGBA image for text with styles - uint32_t* bitmapBuffer = reinterpret_cast< uint32_t* >( glyphData.bitmapBuffer.GetBuffer() ); - uint32_t underlinePixel = *( bitmapBuffer + y * glyphData.width + x ); - uint8_t* underlinePixelBuffer = reinterpret_cast( &underlinePixel ); - - // Write the underline color to the pixel buffer - uint8_t colorAlpha = static_cast< uint8_t >( underlineColor.a * 255.f ); - *( underlinePixelBuffer + 3u ) = colorAlpha; - *( underlinePixelBuffer + 2u ) = static_cast< uint8_t >( underlineColor.b * colorAlpha ); - *( underlinePixelBuffer + 1u ) = static_cast< uint8_t >( underlineColor.g * colorAlpha ); - *( underlinePixelBuffer ) = static_cast< uint8_t >( underlineColor.r * colorAlpha ); - - *( bitmapBuffer + y * glyphData.width + x ) = underlinePixel; - } - } + DrawUnderline(underlineColor, bufferWidth, bufferHeight, glyphData, baseline, currentUnderlinePosition, maxUnderlineThickness, lineExtentLeft, lineExtentRight); } // Draw the background color from the leftmost glyph to the rightmost glyph if ( style == Typesetter::STYLE_BACKGROUND ) { - Vector4 backgroundColor = mModel->GetBackgroundColor(); - - for( int y = glyphData.verticalOffset + baseline - line.ascender; y < glyphData.verticalOffset + baseline - line.descender; y++ ) - { - if( ( y < 0 ) || ( y > static_cast(bufferHeight - 1) ) ) - { - // Do not write out of bounds. - continue; - } - - for( int x = glyphData.horizontalOffset + lineExtentLeft; x <= glyphData.horizontalOffset + lineExtentRight; x++ ) - { - if( ( x < 0 ) || ( x > static_cast(bufferWidth - 1) ) ) - { - // Do not write out of bounds. - continue; - } - - // Always RGBA image for text with styles - uint32_t* bitmapBuffer = reinterpret_cast< uint32_t* >( glyphData.bitmapBuffer.GetBuffer() ); - uint32_t backgroundPixel = *( bitmapBuffer + y * glyphData.width + x ); - uint8_t* backgroundPixelBuffer = reinterpret_cast( &backgroundPixel ); - - // Write the background color to the pixel buffer - uint8_t colorAlpha = static_cast< uint8_t >( backgroundColor.a * 255.f ); - *( backgroundPixelBuffer + 3u ) = colorAlpha; - *( backgroundPixelBuffer + 2u ) = static_cast< uint8_t >( backgroundColor.b * colorAlpha ); - *( backgroundPixelBuffer + 1u ) = static_cast< uint8_t >( backgroundColor.g * colorAlpha ); - *( backgroundPixelBuffer ) = static_cast< uint8_t >( backgroundColor.r * colorAlpha ); - - *( bitmapBuffer + y * glyphData.width + x ) = backgroundPixel; - } - } + DrawBackgroundColor(mModel->GetBackgroundColor(), bufferWidth, bufferHeight, glyphData, baseline, line, lineExtentLeft, lineExtentRight); } // Increases the vertical offset with the line's descender. diff --git a/dali-toolkit/internal/text/text-controller-impl-event-handler.cpp b/dali-toolkit/internal/text/text-controller-impl-event-handler.cpp new file mode 100644 index 0000000..7b50a5c --- /dev/null +++ b/dali-toolkit/internal/text/text-controller-impl-event-handler.cpp @@ -0,0 +1,791 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// CLASS HEADER +#include + +// EXTERNAL INCLUDES +#include +#include + +// INTERNAL INCLUDES +#include + +using namespace Dali; + +namespace +{ + +#if defined(DEBUG_ENABLED) +Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); +#endif + +} // namespace + +namespace Dali +{ + +namespace Toolkit +{ + +namespace Text +{ + +void ControllerImplEventHandler::OnCursorKeyEvent(Controller::Impl& impl, const Event& event) +{ + if( NULL == impl.mEventData || !impl.IsShowingRealText() ) + { + // Nothing to do if there is no text input. + return; + } + + int keyCode = event.p1.mInt; + bool isShiftModifier = event.p2.mBool; + EventData& eventData = *impl.mEventData; + ModelPtr& model = impl.mModel; + LogicalModelPtr& logicalModel = model->mLogicalModel; + VisualModelPtr& visualModel = model->mVisualModel; + + CharacterIndex& primaryCursorPosition = eventData.mPrimaryCursorPosition; + CharacterIndex previousPrimaryCursorPosition = primaryCursorPosition; + + if( Dali::DALI_KEY_CURSOR_LEFT == keyCode ) + { + if( primaryCursorPosition > 0u ) + { + if ( !isShiftModifier && eventData.mDecorator->IsHighlightVisible() ) + { + primaryCursorPosition = std::min(eventData.mLeftSelectionPosition, eventData.mRightSelectionPosition); + } + else + { + primaryCursorPosition = impl.CalculateNewCursorIndex( primaryCursorPosition - 1u ); + } + } + } + else if( Dali::DALI_KEY_CURSOR_RIGHT == keyCode ) + { + if( logicalModel->mText.Count() > primaryCursorPosition ) + { + if ( !isShiftModifier && eventData.mDecorator->IsHighlightVisible() ) + { + primaryCursorPosition = std::max(eventData.mLeftSelectionPosition, eventData.mRightSelectionPosition); + } + else + { + primaryCursorPosition = impl.CalculateNewCursorIndex( primaryCursorPosition ); + } + } + } + else if( Dali::DALI_KEY_CURSOR_UP == keyCode && !isShiftModifier ) + { + // Ignore Shift-Up for text selection for now. + + // Get first the line index of the current cursor position index. + CharacterIndex characterIndex = 0u; + + if( primaryCursorPosition > 0u ) + { + characterIndex = primaryCursorPosition - 1u; + } + + const LineIndex lineIndex = visualModel->GetLineOfCharacter( characterIndex ); + const LineIndex previousLineIndex = ( lineIndex > 0 ? lineIndex - 1u : lineIndex ); + + // Retrieve the cursor position info. + CursorInfo cursorInfo; + impl.GetCursorPosition( primaryCursorPosition, + cursorInfo ); + + // Get the line above. + const LineRun& line = *( visualModel->mLines.Begin() + previousLineIndex ); + + // Get the next hit 'y' point. + const float hitPointY = cursorInfo.lineOffset - 0.5f * ( line.ascender - line.descender ); + + // Use the cursor hook position 'x' and the next hit 'y' position to calculate the new cursor index. + bool matchedCharacter = false; + primaryCursorPosition = Text::GetClosestCursorIndex( visualModel, + logicalModel, + impl.mMetrics, + eventData.mCursorHookPositionX, + hitPointY, + CharacterHitTest::TAP, + matchedCharacter ); + } + else if( Dali::DALI_KEY_CURSOR_DOWN == keyCode && !isShiftModifier ) + { + // Ignore Shift-Down for text selection for now. + + // Get first the line index of the current cursor position index. + CharacterIndex characterIndex = 0u; + + if( primaryCursorPosition > 0u ) + { + characterIndex = primaryCursorPosition - 1u; + } + + const LineIndex lineIndex = visualModel->GetLineOfCharacter( characterIndex ); + + if( lineIndex + 1u < visualModel->mLines.Count() ) + { + // Retrieve the cursor position info. + CursorInfo cursorInfo; + impl.GetCursorPosition( primaryCursorPosition, cursorInfo ); + + // Get the line below. + const LineRun& line = *( visualModel->mLines.Begin() + lineIndex + 1u ); + + // Get the next hit 'y' point. + const float hitPointY = cursorInfo.lineOffset + cursorInfo.lineHeight + 0.5f * ( line.ascender - line.descender ); + + // Use the cursor hook position 'x' and the next hit 'y' position to calculate the new cursor index. + bool matchedCharacter = false; + primaryCursorPosition = Text::GetClosestCursorIndex( visualModel, + logicalModel, + impl.mMetrics, + eventData.mCursorHookPositionX, + hitPointY, + CharacterHitTest::TAP, + matchedCharacter ); + } + } + + if ( !isShiftModifier && eventData.mState != EventData::SELECTING ) + { + // Update selection position after moving the cursor + eventData.mLeftSelectionPosition = primaryCursorPosition; + eventData.mRightSelectionPosition = primaryCursorPosition; + } + + if ( isShiftModifier && impl.IsShowingRealText() && eventData.mShiftSelectionFlag ) + { + // Handle text selection + bool selecting = false; + + if ( Dali::DALI_KEY_CURSOR_LEFT == keyCode || Dali::DALI_KEY_CURSOR_RIGHT == keyCode ) + { + // Shift-Left/Right to select the text + int cursorPositionDelta = primaryCursorPosition - previousPrimaryCursorPosition; + if ( cursorPositionDelta > 0 || eventData.mRightSelectionPosition > 0u ) // Check the boundary + { + eventData.mRightSelectionPosition += cursorPositionDelta; + } + selecting = true; + } + else if ( eventData.mLeftSelectionPosition != eventData.mRightSelectionPosition ) + { + // Show no grab handles and text highlight if Shift-Up/Down pressed but no selected text + selecting = true; + } + + if ( selecting ) + { + // Notify the cursor position to the InputMethodContext. + if( eventData.mInputMethodContext ) + { + eventData.mInputMethodContext.SetCursorPosition( primaryCursorPosition ); + eventData.mInputMethodContext.NotifyCursorPosition(); + } + + impl.ChangeState( EventData::SELECTING ); + + eventData.mUpdateLeftSelectionPosition = true; + eventData.mUpdateRightSelectionPosition = true; + eventData.mUpdateGrabHandlePosition = true; + eventData.mUpdateHighlightBox = true; + + // Hide the text selection popup if select the text using keyboard instead of moving grab handles + if( eventData.mGrabHandlePopupEnabled ) + { + eventData.mDecorator->SetPopupActive( false ); + } + } + } + else + { + // Handle normal cursor move + impl.ChangeState( EventData::EDITING ); + eventData.mUpdateCursorPosition = true; + } + + eventData.mUpdateInputStyle = true; + eventData.mScrollAfterUpdatePosition = true; +} + +void ControllerImplEventHandler::OnTapEvent(Controller::Impl& impl, const Event& event) +{ + if( impl.mEventData ) + { + const unsigned int tapCount = event.p1.mUint; + EventData& eventData = *impl.mEventData; + ModelPtr& model = impl.mModel; + LogicalModelPtr& logicalModel = model->mLogicalModel; + VisualModelPtr& visualModel = model->mVisualModel; + + if( 1u == tapCount ) + { + if( impl.IsShowingRealText() ) + { + // Convert from control's coords to text's coords. + const float xPosition = event.p2.mFloat - model->mScrollPosition.x; + const float yPosition = event.p3.mFloat - model->mScrollPosition.y; + + // Keep the tap 'x' position. Used to move the cursor. + eventData.mCursorHookPositionX = xPosition; + + // Whether to touch point hits on a glyph. + bool matchedCharacter = false; + eventData.mPrimaryCursorPosition = Text::GetClosestCursorIndex( visualModel, + logicalModel, + impl.mMetrics, + xPosition, + yPosition, + CharacterHitTest::TAP, + matchedCharacter ); + + // When the cursor position is changing, delay cursor blinking + eventData.mDecorator->DelayCursorBlink(); + } + else + { + eventData.mPrimaryCursorPosition = 0u; + } + + // Update selection position after tapping + eventData.mLeftSelectionPosition = eventData.mPrimaryCursorPosition; + eventData.mRightSelectionPosition = eventData.mPrimaryCursorPosition; + + eventData.mUpdateCursorPosition = true; + eventData.mUpdateGrabHandlePosition = true; + eventData.mScrollAfterUpdatePosition = true; + eventData.mUpdateInputStyle = true; + + // Notify the cursor position to the InputMethodContext. + if( eventData.mInputMethodContext ) + { + eventData.mInputMethodContext.SetCursorPosition( eventData.mPrimaryCursorPosition ); + eventData.mInputMethodContext.NotifyCursorPosition(); + } + } + else if( 2u == tapCount ) + { + if( eventData.mSelectionEnabled ) + { + // Convert from control's coords to text's coords. + const float xPosition = event.p2.mFloat - model->mScrollPosition.x; + const float yPosition = event.p3.mFloat - model->mScrollPosition.y; + + // Calculates the logical position from the x,y coords. + impl.RepositionSelectionHandles( xPosition, yPosition, eventData.mDoubleTapAction ); + } + } + } +} + +void ControllerImplEventHandler::OnPanEvent(Controller::Impl& impl, const Event& event) +{ + if( impl.mEventData ) + { + EventData& eventData = *impl.mEventData; + DecoratorPtr& decorator = eventData.mDecorator; + + const bool isHorizontalScrollEnabled = decorator->IsHorizontalScrollEnabled(); + const bool isVerticalScrollEnabled = decorator->IsVerticalScrollEnabled(); + + if( !isHorizontalScrollEnabled && !isVerticalScrollEnabled ) + { + // Nothing to do if scrolling is not enabled. + return; + } + + const GestureState state = static_cast( event.p1.mInt ); + switch( state ) + { + case GestureState::STARTED: + { + // Will remove the cursor, handles or text's popup, ... + impl.ChangeState( EventData::TEXT_PANNING ); + break; + } + case GestureState::CONTINUING: + { + ModelPtr& model = impl.mModel; + + const Vector2& layoutSize = model->mVisualModel->GetLayoutSize(); + Vector2& scrollPosition = model->mScrollPosition; + const Vector2 currentScroll = scrollPosition; + + if( isHorizontalScrollEnabled ) + { + const float displacementX = event.p2.mFloat; + scrollPosition.x += displacementX; + + impl.ClampHorizontalScroll( layoutSize ); + } + + if( isVerticalScrollEnabled ) + { + const float displacementY = event.p3.mFloat; + scrollPosition.y += displacementY; + + impl.ClampVerticalScroll( layoutSize ); + } + + decorator->UpdatePositions( scrollPosition - currentScroll ); + break; + } + case GestureState::FINISHED: + case GestureState::CANCELLED: // FALLTHROUGH + { + // Will go back to the previous state to show the cursor, handles, the text's popup, ... + impl.ChangeState( eventData.mPreviousState ); + break; + } + default: + break; + } + } +} + +void ControllerImplEventHandler::OnLongPressEvent(Controller::Impl& impl, const Event& event) +{ + DALI_LOG_INFO( gLogFilter, Debug::General, "Controller::OnLongPressEvent\n" ); + + if( impl.mEventData ) + { + EventData& eventData = *impl.mEventData; + + if( !impl.IsShowingRealText() && ( EventData::EDITING == eventData.mState ) ) + { + impl.ChangeState( EventData::EDITING_WITH_POPUP ); + eventData.mDecoratorUpdated = true; + eventData.mUpdateInputStyle = true; + } + else + { + if( eventData.mSelectionEnabled ) + { + ModelPtr& model = impl.mModel; + + // Convert from control's coords to text's coords. + const float xPosition = event.p2.mFloat - model->mScrollPosition.x; + const float yPosition = event.p3.mFloat - model->mScrollPosition.y; + + // Calculates the logical position from the x,y coords. + impl.RepositionSelectionHandles( xPosition, yPosition, eventData.mLongPressAction ); + } + } + } +} + +void ControllerImplEventHandler::OnHandleEvent(Controller::Impl& impl, const Event& event) +{ + if( impl.mEventData ) + { + const unsigned int state = event.p1.mUint; + const bool handleStopScrolling = ( HANDLE_STOP_SCROLLING == state ); + const bool isSmoothHandlePanEnabled = impl.mEventData->mDecorator->IsSmoothHandlePanEnabled(); + + if( HANDLE_PRESSED == state ) + { + OnHandlePressed(impl, event, isSmoothHandlePanEnabled); + } // end ( HANDLE_PRESSED == state ) + else if( ( HANDLE_RELEASED == state ) || + handleStopScrolling ) + { + OnHandleReleased(impl, event, isSmoothHandlePanEnabled, handleStopScrolling); + } // end ( ( HANDLE_RELEASED == state ) || ( HANDLE_STOP_SCROLLING == state ) ) + else if( HANDLE_SCROLLING == state ) + { + OnHandleScrolling(impl, event, isSmoothHandlePanEnabled); + } // end ( HANDLE_SCROLLING == state ) + } +} + +void ControllerImplEventHandler::OnSelectEvent(Controller::Impl& impl, const Event& event ) +{ + if( impl.mEventData && impl.mEventData->mSelectionEnabled ) + { + ModelPtr& model = impl.mModel; + const Vector2& scrollPosition = model->mScrollPosition; + + // Convert from control's coords to text's coords. + const float xPosition = event.p2.mFloat - scrollPosition.x; + const float yPosition = event.p3.mFloat - scrollPosition.y; + + // Calculates the logical position from the x,y coords. + impl.RepositionSelectionHandles( xPosition, yPosition, Controller::NoTextTap::HIGHLIGHT ); + } +} + +void ControllerImplEventHandler::OnSelectAllEvent(Controller::Impl& impl) +{ + DALI_LOG_INFO( gLogFilter, Debug::Verbose, "OnSelectAllEvent mEventData->mSelectionEnabled%s \n", impl.mEventData->mSelectionEnabled?"true":"false"); + + if( impl.mEventData ) + { + EventData& eventData = *impl.mEventData; + if( eventData.mSelectionEnabled ) + { + ModelPtr& model = impl.mModel; + const Vector2& scrollPosition = model->mScrollPosition; + + // Calculates the logical position from the start. + impl.RepositionSelectionHandles( 0.f - scrollPosition.x, + 0.f - scrollPosition.y, + Controller::NoTextTap::HIGHLIGHT ); + + eventData.mLeftSelectionPosition = 0u; + eventData.mRightSelectionPosition = model->mLogicalModel->mText.Count(); + } + } +} + +void ControllerImplEventHandler::OnSelectNoneEvent(Controller::Impl& impl) +{ + DALI_LOG_INFO( gLogFilter, Debug::Verbose, "OnSelectNoneEvent mEventData->mSelectionEnabled%s \n", impl.mEventData->mSelectionEnabled?"true":"false"); + + if( impl.mEventData ) + { + EventData& eventData = *impl.mEventData; + if( eventData.mSelectionEnabled && eventData.mState == EventData::SELECTING) + { + eventData.mPrimaryCursorPosition = 0u; + eventData.mLeftSelectionPosition = eventData.mRightSelectionPosition = eventData.mPrimaryCursorPosition; + impl.ChangeState( EventData::INACTIVE ); + eventData.mUpdateCursorPosition = true; + eventData.mUpdateInputStyle = true; + eventData.mScrollAfterUpdatePosition = true; + } + } +} + +void ControllerImplEventHandler::OnHandlePressed(Controller::Impl& impl, const Event& event, const bool isSmoothHandlePanEnabled) +{ + ModelPtr& model = impl.mModel; + const Vector2& scrollPosition = model->mScrollPosition; + + // Convert from decorator's coords to text's coords. + const float xPosition = event.p2.mFloat - scrollPosition.x; + const float yPosition = event.p3.mFloat - scrollPosition.y; + + // Need to calculate the handle's new position. + bool matchedCharacter = false; + const CharacterIndex handleNewPosition = Text::GetClosestCursorIndex( model->mVisualModel, + model->mLogicalModel, + impl.mMetrics, + xPosition, + yPosition, + CharacterHitTest::SCROLL, + matchedCharacter ); + + EventData& eventData = *impl.mEventData; + + if( Event::GRAB_HANDLE_EVENT == event.type ) + { + impl.ChangeState ( EventData::GRAB_HANDLE_PANNING ); + + if( handleNewPosition != eventData.mPrimaryCursorPosition ) + { + // Updates the cursor position if the handle's new position is different than the current one. + eventData.mUpdateCursorPosition = true; + // Does not update the grab handle position if the smooth panning is enabled. (The decorator does it smooth). + eventData.mUpdateGrabHandlePosition = !isSmoothHandlePanEnabled; + eventData.mPrimaryCursorPosition = handleNewPosition; + } + + // Updates the decorator if the soft handle panning is enabled. It triggers a relayout in the decorator and the new position of the handle is set. + eventData.mDecoratorUpdated = isSmoothHandlePanEnabled; + } + else if( Event::LEFT_SELECTION_HANDLE_EVENT == event.type ) + { + impl.ChangeState ( EventData::SELECTION_HANDLE_PANNING ); + + if( ( handleNewPosition != eventData.mLeftSelectionPosition ) && + ( handleNewPosition != eventData.mRightSelectionPosition ) ) + { + // Updates the highlight box if the handle's new position is different than the current one. + eventData.mUpdateHighlightBox = true; + // Does not update the selection handle position if the smooth panning is enabled. (The decorator does it smooth). + eventData.mUpdateLeftSelectionPosition = !isSmoothHandlePanEnabled; + eventData.mLeftSelectionPosition = handleNewPosition; + } + + // Updates the decorator if the soft handle panning is enabled. It triggers a relayout in the decorator and the new position of the handle is set. + eventData.mDecoratorUpdated = isSmoothHandlePanEnabled; + + // Will define the order to scroll the text to match the handle position. + eventData.mIsLeftHandleSelected = true; + eventData.mIsRightHandleSelected = false; + } + else if( Event::RIGHT_SELECTION_HANDLE_EVENT == event.type ) + { + impl.ChangeState ( EventData::SELECTION_HANDLE_PANNING ); + + if( ( handleNewPosition != eventData.mRightSelectionPosition ) && + ( handleNewPosition != eventData.mLeftSelectionPosition ) ) + { + // Updates the highlight box if the handle's new position is different than the current one. + eventData.mUpdateHighlightBox = true; + // Does not update the selection handle position if the smooth panning is enabled. (The decorator does it smooth). + eventData.mUpdateRightSelectionPosition = !isSmoothHandlePanEnabled; + eventData.mRightSelectionPosition = handleNewPosition; + } + + // Updates the decorator if the soft handle panning is enabled. It triggers a relayout in the decorator and the new position of the handle is set. + eventData.mDecoratorUpdated = isSmoothHandlePanEnabled; + + // Will define the order to scroll the text to match the handle position. + eventData.mIsLeftHandleSelected = false; + eventData.mIsRightHandleSelected = true; + } +} + +void ControllerImplEventHandler::OnHandleReleased(Controller::Impl& impl, const Event& event, const bool isSmoothHandlePanEnabled, const bool handleStopScrolling) +{ + CharacterIndex handlePosition = 0u; + if( handleStopScrolling || isSmoothHandlePanEnabled ) + { + ModelPtr& model = impl.mModel; + const Vector2& scrollPosition = model->mScrollPosition; + + // Convert from decorator's coords to text's coords. + const float xPosition = event.p2.mFloat - scrollPosition.x; + const float yPosition = event.p3.mFloat - scrollPosition.y; + + bool matchedCharacter = false; + handlePosition = Text::GetClosestCursorIndex( model->mVisualModel, + model->mLogicalModel, + impl.mMetrics, + xPosition, + yPosition, + CharacterHitTest::SCROLL, + matchedCharacter ); + } + + EventData& eventData = *impl.mEventData; + + if( Event::GRAB_HANDLE_EVENT == event.type ) + { + eventData.mUpdateCursorPosition = true; + eventData.mUpdateGrabHandlePosition = true; + eventData.mUpdateInputStyle = true; + + if( !impl.IsClipboardEmpty() ) + { + impl.ChangeState( EventData::EDITING_WITH_PASTE_POPUP ); // Moving grabhandle will show Paste Popup + } + + if( handleStopScrolling || isSmoothHandlePanEnabled ) + { + eventData.mScrollAfterUpdatePosition = true; + eventData.mPrimaryCursorPosition = handlePosition; + } + } + else if( Event::LEFT_SELECTION_HANDLE_EVENT == event.type ) + { + impl.ChangeState( EventData::SELECTING ); + + eventData.mUpdateHighlightBox = true; + eventData.mUpdateLeftSelectionPosition = true; + eventData.mUpdateRightSelectionPosition = true; + + if( handleStopScrolling || isSmoothHandlePanEnabled ) + { + eventData.mScrollAfterUpdatePosition = true; + + if( ( handlePosition != eventData.mRightSelectionPosition ) && + ( handlePosition != eventData.mLeftSelectionPosition ) ) + { + eventData.mLeftSelectionPosition = handlePosition; + } + } + } + else if( Event::RIGHT_SELECTION_HANDLE_EVENT == event.type ) + { + impl.ChangeState( EventData::SELECTING ); + + eventData.mUpdateHighlightBox = true; + eventData.mUpdateRightSelectionPosition = true; + eventData.mUpdateLeftSelectionPosition = true; + + if( handleStopScrolling || isSmoothHandlePanEnabled ) + { + eventData.mScrollAfterUpdatePosition = true; + if( ( handlePosition != eventData.mRightSelectionPosition ) && + ( handlePosition != eventData.mLeftSelectionPosition ) ) + { + eventData.mRightSelectionPosition = handlePosition; + } + } + } + + eventData.mDecoratorUpdated = true; +} + +void ControllerImplEventHandler::OnHandleScrolling(Controller::Impl& impl, const Event& event, const bool isSmoothHandlePanEnabled) +{ + ModelPtr& model = impl.mModel; + Vector2& scrollPosition = model->mScrollPosition; + VisualModelPtr& visualModel = model->mVisualModel; + + const float xSpeed = event.p2.mFloat; + const float ySpeed = event.p3.mFloat; + const Vector2& layoutSize = visualModel->GetLayoutSize(); + const Vector2 currentScrollPosition = scrollPosition; + + scrollPosition.x += xSpeed; + scrollPosition.y += ySpeed; + + impl.ClampHorizontalScroll( layoutSize ); + impl.ClampVerticalScroll( layoutSize ); + + EventData& eventData = *impl.mEventData; + DecoratorPtr& decorator = eventData.mDecorator; + + bool endOfScroll = false; + if( Vector2::ZERO == ( currentScrollPosition - scrollPosition ) ) + { + // Notify the decorator there is no more text to scroll. + // The decorator won't send more scroll events. + decorator->NotifyEndOfScroll(); + // Still need to set the position of the handle. + endOfScroll = true; + } + + // Set the position of the handle. + const bool scrollRightDirection = xSpeed > 0.f; + const bool scrollBottomDirection = ySpeed > 0.f; + const bool leftSelectionHandleEvent = Event::LEFT_SELECTION_HANDLE_EVENT == event.type; + const bool rightSelectionHandleEvent = Event::RIGHT_SELECTION_HANDLE_EVENT == event.type; + + if( Event::GRAB_HANDLE_EVENT == event.type ) + { + impl.ChangeState( EventData::GRAB_HANDLE_PANNING ); + + // Get the grab handle position in decorator coords. + Vector2 position = decorator->GetPosition( GRAB_HANDLE ); + + if( decorator->IsHorizontalScrollEnabled() ) + { + // Position the grag handle close to either the left or right edge. + position.x = scrollRightDirection ? 0.f : visualModel->mControlSize.width; + } + + if( decorator->IsVerticalScrollEnabled() ) + { + position.x = eventData.mCursorHookPositionX; + + // Position the grag handle close to either the top or bottom edge. + position.y = scrollBottomDirection ? 0.f : visualModel->mControlSize.height; + } + + // Get the new handle position. + // The grab handle's position is in decorator's coords. Need to transforms to text's coords. + bool matchedCharacter = false; + const CharacterIndex handlePosition = Text::GetClosestCursorIndex( visualModel, + impl.mModel->mLogicalModel, + impl.mMetrics, + position.x - scrollPosition.x, + position.y - scrollPosition.y, + CharacterHitTest::SCROLL, + matchedCharacter ); + + if( eventData.mPrimaryCursorPosition != handlePosition ) + { + eventData.mUpdateCursorPosition = true; + eventData.mUpdateGrabHandlePosition = !isSmoothHandlePanEnabled; + eventData.mScrollAfterUpdatePosition = true; + eventData.mPrimaryCursorPosition = handlePosition; + } + eventData.mUpdateInputStyle = eventData.mUpdateCursorPosition; + + // Updates the decorator if the soft handle panning is enabled. + eventData.mDecoratorUpdated = isSmoothHandlePanEnabled; + } + else if( leftSelectionHandleEvent || rightSelectionHandleEvent ) + { + impl.ChangeState( EventData::SELECTION_HANDLE_PANNING ); + + // Get the selection handle position in decorator coords. + Vector2 position = decorator->GetPosition( leftSelectionHandleEvent ? Text::LEFT_SELECTION_HANDLE : Text::RIGHT_SELECTION_HANDLE ); + + if( decorator->IsHorizontalScrollEnabled() ) + { + // Position the selection handle close to either the left or right edge. + position.x = scrollRightDirection ? 0.f : visualModel->mControlSize.width; + } + + if( decorator->IsVerticalScrollEnabled() ) + { + position.x = eventData.mCursorHookPositionX; + + // Position the grag handle close to either the top or bottom edge. + position.y = scrollBottomDirection ? 0.f : visualModel->mControlSize.height; + } + + // Get the new handle position. + // The selection handle's position is in decorator's coords. Need to transform to text's coords. + bool matchedCharacter = false; + const CharacterIndex handlePosition = Text::GetClosestCursorIndex( visualModel, + impl.mModel->mLogicalModel, + impl.mMetrics, + position.x - scrollPosition.x, + position.y - scrollPosition.y, + CharacterHitTest::SCROLL, + matchedCharacter ); + + if( leftSelectionHandleEvent ) + { + const bool differentHandles = ( eventData.mLeftSelectionPosition != handlePosition ) && ( eventData.mRightSelectionPosition != handlePosition ); + + if( differentHandles || endOfScroll ) + { + eventData.mUpdateHighlightBox = true; + eventData.mUpdateLeftSelectionPosition = !isSmoothHandlePanEnabled; + eventData.mUpdateRightSelectionPosition = isSmoothHandlePanEnabled; + eventData.mLeftSelectionPosition = handlePosition; + } + } + else + { + const bool differentHandles = ( eventData.mRightSelectionPosition != handlePosition ) && ( eventData.mLeftSelectionPosition != handlePosition ); + if( differentHandles || endOfScroll ) + { + eventData.mUpdateHighlightBox = true; + eventData.mUpdateRightSelectionPosition = !isSmoothHandlePanEnabled; + eventData.mUpdateLeftSelectionPosition = isSmoothHandlePanEnabled; + eventData.mRightSelectionPosition = handlePosition; + } + } + + if( eventData.mUpdateLeftSelectionPosition || eventData.mUpdateRightSelectionPosition ) + { + impl.RepositionSelectionHandles(); + + eventData.mScrollAfterUpdatePosition = !isSmoothHandlePanEnabled; + } + } + eventData.mDecoratorUpdated = true; +} + +} // namespace Text + +} // namespace Toolkit + +} // namespace Dali diff --git a/dali-toolkit/internal/text/text-controller-impl-event-handler.h b/dali-toolkit/internal/text/text-controller-impl-event-handler.h new file mode 100644 index 0000000..52ac94a --- /dev/null +++ b/dali-toolkit/internal/text/text-controller-impl-event-handler.h @@ -0,0 +1,139 @@ +#ifndef DALI_TOOLKIT_TEXT_CONTROLLER_IMPL_EVENT_HANDLER_H +#define DALI_TOOLKIT_TEXT_CONTROLLER_IMPL_EVENT_HANDLER_H + +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// INTERNAL INCLUDES +#include + +namespace Dali +{ + +namespace Toolkit +{ + +namespace Text +{ + +/** + * Contains all the event handling methods for Text::Controller::Impl + */ +struct ControllerImplEventHandler +{ + /** + * @brief Called by Controller::Impl when a cursor key event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnCursorKeyEvent(Controller::Impl& controllerImpl, const Event& event); + + /** + * @brief Called by Controller::Impl when a tap event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnTapEvent(Controller::Impl& controllerImpl, const Event& event); + + /** + * @brief Called by Controller::Impl when a pan event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnPanEvent(Controller::Impl& controllerImpl, const Event& event); + + /** + * @brief Called by Controller::Impl when a long press event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnLongPressEvent(Controller::Impl& controllerImpl, const Event& event); + + /** + * @brief Called by Controller::Impl when a handle event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnHandleEvent(Controller::Impl& controllerImpl, const Event& event); + + /** + * @brief Called by Controller::Impl when a select event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnSelectEvent(Controller::Impl& controllerImpl, const Event& event ); + + /** + * @brief Called by Controller::Impl when a select all event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnSelectAllEvent(Controller::Impl& controllerImpl); + + /** + * @brief Called by Controller::Impl when a select none event is received. + * + * @param controllerImpl A reference to Controller::Impl + * @param event The event + */ + static void OnSelectNoneEvent(Controller::Impl& controllerImpl); + +private: + + /** + * @brief Called by OnHandleEvent when we are in the Pressed state. + * + * @param impl A reference to Controller::Impl + * @param event The event + * @param isSmoothHandlePanEnabled Whether smooth handle pan is enabled + */ + static void OnHandlePressed(Controller::Impl& impl, const Event& event, const bool isSmoothHandlePanEnabled); + + /** + * @brief Called by OnHandleEvent when we are in the Released state. + * + * @param impl A reference to Controller::Impl + * @param event The event + * @param isSmoothHandlePanEnabled Whether smooth handle pan is enabled + * @param handleStopScrolling Whether we should handle stop scrolling or not + */ + static void OnHandleReleased(Controller::Impl& impl, const Event& event, const bool isSmoothHandlePanEnabled, const bool handleStopScrolling); + + /** + * @brief Called by OnHandleEvent when we are in the Scrolling state. + * + * @param impl A reference to Controller::Impl + * @param event The event + * @param isSmoothHandlePanEnabled Whether smooth handle pan is enabled + */ + static void OnHandleScrolling(Controller::Impl& impl, const Event& event, const bool isSmoothHandlePanEnabled); +}; + +} // namespace Text + +} // namespace Toolkit + +} // namespace Dali + +#endif // DALI_TOOLKIT_TEXT_CONTROLLER_IMPL_EVENT_HANDLER_H diff --git a/dali-toolkit/internal/text/text-controller-impl.cpp b/dali-toolkit/internal/text/text-controller-impl.cpp index bfce755..0cd6adb 100644 --- a/dali-toolkit/internal/text/text-controller-impl.cpp +++ b/dali-toolkit/internal/text/text-controller-impl.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * Copyright (c) 2020 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ #include // EXTERNAL INCLUDES -#include #include #include #include @@ -34,6 +33,7 @@ #include #include #include +#include #include using namespace Dali; @@ -53,7 +53,7 @@ struct SelectionBoxInfo }; #if defined(DEBUG_ENABLED) - Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); +Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); #endif const float MAX_FLOAT = std::numeric_limits::max(); @@ -1341,704 +1341,42 @@ float Controller::Impl::GetDefaultFontLineHeight() void Controller::Impl::OnCursorKeyEvent( const Event& event ) { - if( NULL == mEventData || !IsShowingRealText() ) - { - // Nothing to do if there is no text input. - return; - } - - int keyCode = event.p1.mInt; - bool isShiftModifier = event.p2.mBool; - - CharacterIndex previousPrimaryCursorPosition = mEventData->mPrimaryCursorPosition; - - if( Dali::DALI_KEY_CURSOR_LEFT == keyCode ) - { - if( mEventData->mPrimaryCursorPosition > 0u ) - { - if ( !isShiftModifier && mEventData->mDecorator->IsHighlightVisible() ) - { - mEventData->mPrimaryCursorPosition = std::min(mEventData->mLeftSelectionPosition, mEventData->mRightSelectionPosition); - } - else - { - mEventData->mPrimaryCursorPosition = CalculateNewCursorIndex( mEventData->mPrimaryCursorPosition - 1u ); - } - } - } - else if( Dali::DALI_KEY_CURSOR_RIGHT == keyCode ) - { - if( mModel->mLogicalModel->mText.Count() > mEventData->mPrimaryCursorPosition ) - { - if ( !isShiftModifier && mEventData->mDecorator->IsHighlightVisible() ) - { - mEventData->mPrimaryCursorPosition = std::max(mEventData->mLeftSelectionPosition, mEventData->mRightSelectionPosition); - } - else - { - mEventData->mPrimaryCursorPosition = CalculateNewCursorIndex( mEventData->mPrimaryCursorPosition ); - } - } - } - else if( Dali::DALI_KEY_CURSOR_UP == keyCode && !isShiftModifier ) - { - // Ignore Shift-Up for text selection for now. - - // Get first the line index of the current cursor position index. - CharacterIndex characterIndex = 0u; - - if( mEventData->mPrimaryCursorPosition > 0u ) - { - characterIndex = mEventData->mPrimaryCursorPosition - 1u; - } - - const LineIndex lineIndex = mModel->mVisualModel->GetLineOfCharacter( characterIndex ); - const LineIndex previousLineIndex = ( lineIndex > 0 ? lineIndex - 1u : lineIndex ); - - // Retrieve the cursor position info. - CursorInfo cursorInfo; - GetCursorPosition( mEventData->mPrimaryCursorPosition, - cursorInfo ); - - // Get the line above. - const LineRun& line = *( mModel->mVisualModel->mLines.Begin() + previousLineIndex ); - - // Get the next hit 'y' point. - const float hitPointY = cursorInfo.lineOffset - 0.5f * ( line.ascender - line.descender ); - - // Use the cursor hook position 'x' and the next hit 'y' position to calculate the new cursor index. - bool matchedCharacter = false; - mEventData->mPrimaryCursorPosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - mEventData->mCursorHookPositionX, - hitPointY, - CharacterHitTest::TAP, - matchedCharacter ); - } - else if( Dali::DALI_KEY_CURSOR_DOWN == keyCode && !isShiftModifier ) - { - // Ignore Shift-Down for text selection for now. - - // Get first the line index of the current cursor position index. - CharacterIndex characterIndex = 0u; - - if( mEventData->mPrimaryCursorPosition > 0u ) - { - characterIndex = mEventData->mPrimaryCursorPosition - 1u; - } - - const LineIndex lineIndex = mModel->mVisualModel->GetLineOfCharacter( characterIndex ); - - if( lineIndex + 1u < mModel->mVisualModel->mLines.Count() ) - { - // Retrieve the cursor position info. - CursorInfo cursorInfo; - GetCursorPosition( mEventData->mPrimaryCursorPosition, - cursorInfo ); - - // Get the line below. - const LineRun& line = *( mModel->mVisualModel->mLines.Begin() + lineIndex + 1u ); - - // Get the next hit 'y' point. - const float hitPointY = cursorInfo.lineOffset + cursorInfo.lineHeight + 0.5f * ( line.ascender - line.descender ); - - // Use the cursor hook position 'x' and the next hit 'y' position to calculate the new cursor index. - bool matchedCharacter = false; - mEventData->mPrimaryCursorPosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - mEventData->mCursorHookPositionX, - hitPointY, - CharacterHitTest::TAP, - matchedCharacter ); - } - } - - if ( !isShiftModifier && mEventData->mState != EventData::SELECTING ) - { - // Update selection position after moving the cursor - mEventData->mLeftSelectionPosition = mEventData->mPrimaryCursorPosition; - mEventData->mRightSelectionPosition = mEventData->mPrimaryCursorPosition; - } - - if ( isShiftModifier && IsShowingRealText() && mEventData->mShiftSelectionFlag ) - { - // Handle text selection - bool selecting = false; - - if ( Dali::DALI_KEY_CURSOR_LEFT == keyCode || Dali::DALI_KEY_CURSOR_RIGHT == keyCode ) - { - // Shift-Left/Right to select the text - int cursorPositionDelta = mEventData->mPrimaryCursorPosition - previousPrimaryCursorPosition; - if ( cursorPositionDelta > 0 || mEventData->mRightSelectionPosition > 0u ) // Check the boundary - { - mEventData->mRightSelectionPosition += cursorPositionDelta; - } - selecting = true; - } - else if ( mEventData->mLeftSelectionPosition != mEventData->mRightSelectionPosition ) - { - // Show no grab handles and text highlight if Shift-Up/Down pressed but no selected text - selecting = true; - } - - if ( selecting ) - { - // Notify the cursor position to the InputMethodContext. - if( mEventData->mInputMethodContext ) - { - mEventData->mInputMethodContext.SetCursorPosition( mEventData->mPrimaryCursorPosition ); - mEventData->mInputMethodContext.NotifyCursorPosition(); - } - - ChangeState( EventData::SELECTING ); - - mEventData->mUpdateLeftSelectionPosition = true; - mEventData->mUpdateRightSelectionPosition = true; - mEventData->mUpdateGrabHandlePosition = true; - mEventData->mUpdateHighlightBox = true; - - // Hide the text selection popup if select the text using keyboard instead of moving grab handles - if( mEventData->mGrabHandlePopupEnabled ) - { - mEventData->mDecorator->SetPopupActive( false ); - } - } - } - else - { - // Handle normal cursor move - ChangeState( EventData::EDITING ); - mEventData->mUpdateCursorPosition = true; - } - - mEventData->mUpdateInputStyle = true; - mEventData->mScrollAfterUpdatePosition = true; + ControllerImplEventHandler::OnCursorKeyEvent(*this, event); } void Controller::Impl::OnTapEvent( const Event& event ) { - if( NULL != mEventData ) - { - const unsigned int tapCount = event.p1.mUint; - - if( 1u == tapCount ) - { - if( IsShowingRealText() ) - { - // Convert from control's coords to text's coords. - const float xPosition = event.p2.mFloat - mModel->mScrollPosition.x; - const float yPosition = event.p3.mFloat - mModel->mScrollPosition.y; - - // Keep the tap 'x' position. Used to move the cursor. - mEventData->mCursorHookPositionX = xPosition; - - // Whether to touch point hits on a glyph. - bool matchedCharacter = false; - mEventData->mPrimaryCursorPosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - xPosition, - yPosition, - CharacterHitTest::TAP, - matchedCharacter ); - - // When the cursor position is changing, delay cursor blinking - mEventData->mDecorator->DelayCursorBlink(); - } - else - { - mEventData->mPrimaryCursorPosition = 0u; - } - - // Update selection position after tapping - mEventData->mLeftSelectionPosition = mEventData->mPrimaryCursorPosition; - mEventData->mRightSelectionPosition = mEventData->mPrimaryCursorPosition; - - mEventData->mUpdateCursorPosition = true; - mEventData->mUpdateGrabHandlePosition = true; - mEventData->mScrollAfterUpdatePosition = true; - mEventData->mUpdateInputStyle = true; - - // Notify the cursor position to the InputMethodContext. - if( mEventData->mInputMethodContext ) - { - mEventData->mInputMethodContext.SetCursorPosition( mEventData->mPrimaryCursorPosition ); - mEventData->mInputMethodContext.NotifyCursorPosition(); - } - } - else if( 2u == tapCount ) - { - if( mEventData->mSelectionEnabled ) - { - // Convert from control's coords to text's coords. - const float xPosition = event.p2.mFloat - mModel->mScrollPosition.x; - const float yPosition = event.p3.mFloat - mModel->mScrollPosition.y; - - // Calculates the logical position from the x,y coords. - RepositionSelectionHandles( xPosition, - yPosition, - mEventData->mDoubleTapAction ); - } - } - } + ControllerImplEventHandler::OnTapEvent(*this, event); } void Controller::Impl::OnPanEvent( const Event& event ) { - if( NULL == mEventData ) - { - // Nothing to do if there is no text input. - return; - } - - const bool isHorizontalScrollEnabled = mEventData->mDecorator->IsHorizontalScrollEnabled(); - const bool isVerticalScrollEnabled = mEventData->mDecorator->IsVerticalScrollEnabled(); - - if( !isHorizontalScrollEnabled && !isVerticalScrollEnabled ) - { - // Nothing to do if scrolling is not enabled. - return; - } - - const GestureState state = static_cast( event.p1.mInt ); - switch( state ) - { - case GestureState::STARTED: - { - // Will remove the cursor, handles or text's popup, ... - ChangeState( EventData::TEXT_PANNING ); - break; - } - case GestureState::CONTINUING: - { - const Vector2& layoutSize = mModel->mVisualModel->GetLayoutSize(); - const Vector2 currentScroll = mModel->mScrollPosition; - - if( isHorizontalScrollEnabled ) - { - const float displacementX = event.p2.mFloat; - mModel->mScrollPosition.x += displacementX; - - ClampHorizontalScroll( layoutSize ); - } - - if( isVerticalScrollEnabled ) - { - const float displacementY = event.p3.mFloat; - mModel->mScrollPosition.y += displacementY; - - ClampVerticalScroll( layoutSize ); - } - - mEventData->mDecorator->UpdatePositions( mModel->mScrollPosition - currentScroll ); - break; - } - case GestureState::FINISHED: - case GestureState::CANCELLED: // FALLTHROUGH - { - // Will go back to the previous state to show the cursor, handles, the text's popup, ... - ChangeState( mEventData->mPreviousState ); - break; - } - default: - break; - } + ControllerImplEventHandler::OnPanEvent(*this, event); } void Controller::Impl::OnLongPressEvent( const Event& event ) { - DALI_LOG_INFO( gLogFilter, Debug::General, "Controller::OnLongPressEvent\n" ); - - if( !IsShowingRealText() && ( EventData::EDITING == mEventData->mState ) ) - { - ChangeState( EventData::EDITING_WITH_POPUP ); - mEventData->mDecoratorUpdated = true; - mEventData->mUpdateInputStyle = true; - } - else - { - if( mEventData->mSelectionEnabled ) - { - // Convert from control's coords to text's coords. - const float xPosition = event.p2.mFloat - mModel->mScrollPosition.x; - const float yPosition = event.p3.mFloat - mModel->mScrollPosition.y; - - // Calculates the logical position from the x,y coords. - RepositionSelectionHandles( xPosition, - yPosition, - mEventData->mLongPressAction ); - } - } + ControllerImplEventHandler::OnLongPressEvent(*this, event); } void Controller::Impl::OnHandleEvent( const Event& event ) { - if( NULL == mEventData ) - { - // Nothing to do if there is no text input. - return; - } - - const unsigned int state = event.p1.mUint; - const bool handleStopScrolling = ( HANDLE_STOP_SCROLLING == state ); - const bool isSmoothHandlePanEnabled = mEventData->mDecorator->IsSmoothHandlePanEnabled(); - - if( HANDLE_PRESSED == state ) - { - // Convert from decorator's coords to text's coords. - const float xPosition = event.p2.mFloat - mModel->mScrollPosition.x; - const float yPosition = event.p3.mFloat - mModel->mScrollPosition.y; - - // Need to calculate the handle's new position. - bool matchedCharacter = false; - const CharacterIndex handleNewPosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - xPosition, - yPosition, - CharacterHitTest::SCROLL, - matchedCharacter ); - - if( Event::GRAB_HANDLE_EVENT == event.type ) - { - ChangeState ( EventData::GRAB_HANDLE_PANNING ); - - if( handleNewPosition != mEventData->mPrimaryCursorPosition ) - { - // Updates the cursor position if the handle's new position is different than the current one. - mEventData->mUpdateCursorPosition = true; - // Does not update the grab handle position if the smooth panning is enabled. (The decorator does it smooth). - mEventData->mUpdateGrabHandlePosition = !isSmoothHandlePanEnabled; - mEventData->mPrimaryCursorPosition = handleNewPosition; - } - - // Updates the decorator if the soft handle panning is enabled. It triggers a relayout in the decorator and the new position of the handle is set. - mEventData->mDecoratorUpdated = isSmoothHandlePanEnabled; - } - else if( Event::LEFT_SELECTION_HANDLE_EVENT == event.type ) - { - ChangeState ( EventData::SELECTION_HANDLE_PANNING ); - - if( ( handleNewPosition != mEventData->mLeftSelectionPosition ) && - ( handleNewPosition != mEventData->mRightSelectionPosition ) ) - { - // Updates the highlight box if the handle's new position is different than the current one. - mEventData->mUpdateHighlightBox = true; - // Does not update the selection handle position if the smooth panning is enabled. (The decorator does it smooth). - mEventData->mUpdateLeftSelectionPosition = !isSmoothHandlePanEnabled; - mEventData->mLeftSelectionPosition = handleNewPosition; - } - - // Updates the decorator if the soft handle panning is enabled. It triggers a relayout in the decorator and the new position of the handle is set. - mEventData->mDecoratorUpdated = isSmoothHandlePanEnabled; - - // Will define the order to scroll the text to match the handle position. - mEventData->mIsLeftHandleSelected = true; - mEventData->mIsRightHandleSelected = false; - } - else if( Event::RIGHT_SELECTION_HANDLE_EVENT == event.type ) - { - ChangeState ( EventData::SELECTION_HANDLE_PANNING ); - - if( ( handleNewPosition != mEventData->mRightSelectionPosition ) && - ( handleNewPosition != mEventData->mLeftSelectionPosition ) ) - { - // Updates the highlight box if the handle's new position is different than the current one. - mEventData->mUpdateHighlightBox = true; - // Does not update the selection handle position if the smooth panning is enabled. (The decorator does it smooth). - mEventData->mUpdateRightSelectionPosition = !isSmoothHandlePanEnabled; - mEventData->mRightSelectionPosition = handleNewPosition; - } - - // Updates the decorator if the soft handle panning is enabled. It triggers a relayout in the decorator and the new position of the handle is set. - mEventData->mDecoratorUpdated = isSmoothHandlePanEnabled; - - // Will define the order to scroll the text to match the handle position. - mEventData->mIsLeftHandleSelected = false; - mEventData->mIsRightHandleSelected = true; - } - } // end ( HANDLE_PRESSED == state ) - else if( ( HANDLE_RELEASED == state ) || - handleStopScrolling ) - { - CharacterIndex handlePosition = 0u; - if( handleStopScrolling || isSmoothHandlePanEnabled ) - { - // Convert from decorator's coords to text's coords. - const float xPosition = event.p2.mFloat - mModel->mScrollPosition.x; - const float yPosition = event.p3.mFloat - mModel->mScrollPosition.y; - - bool matchedCharacter = false; - handlePosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - xPosition, - yPosition, - CharacterHitTest::SCROLL, - matchedCharacter ); - } - - if( Event::GRAB_HANDLE_EVENT == event.type ) - { - mEventData->mUpdateCursorPosition = true; - mEventData->mUpdateGrabHandlePosition = true; - mEventData->mUpdateInputStyle = true; - - if( !IsClipboardEmpty() ) - { - ChangeState( EventData::EDITING_WITH_PASTE_POPUP ); // Moving grabhandle will show Paste Popup - } - - if( handleStopScrolling || isSmoothHandlePanEnabled ) - { - mEventData->mScrollAfterUpdatePosition = true; - mEventData->mPrimaryCursorPosition = handlePosition; - } - } - else if( Event::LEFT_SELECTION_HANDLE_EVENT == event.type ) - { - ChangeState( EventData::SELECTING ); - - mEventData->mUpdateHighlightBox = true; - mEventData->mUpdateLeftSelectionPosition = true; - mEventData->mUpdateRightSelectionPosition = true; - - if( handleStopScrolling || isSmoothHandlePanEnabled ) - { - mEventData->mScrollAfterUpdatePosition = true; - - if( ( handlePosition != mEventData->mRightSelectionPosition ) && - ( handlePosition != mEventData->mLeftSelectionPosition ) ) - { - mEventData->mLeftSelectionPosition = handlePosition; - } - } - } - else if( Event::RIGHT_SELECTION_HANDLE_EVENT == event.type ) - { - ChangeState( EventData::SELECTING ); - - mEventData->mUpdateHighlightBox = true; - mEventData->mUpdateRightSelectionPosition = true; - mEventData->mUpdateLeftSelectionPosition = true; - - if( handleStopScrolling || isSmoothHandlePanEnabled ) - { - mEventData->mScrollAfterUpdatePosition = true; - if( ( handlePosition != mEventData->mRightSelectionPosition ) && - ( handlePosition != mEventData->mLeftSelectionPosition ) ) - { - mEventData->mRightSelectionPosition = handlePosition; - } - } - } - - mEventData->mDecoratorUpdated = true; - } // end ( ( HANDLE_RELEASED == state ) || ( HANDLE_STOP_SCROLLING == state ) ) - else if( HANDLE_SCROLLING == state ) - { - const float xSpeed = event.p2.mFloat; - const float ySpeed = event.p3.mFloat; - const Vector2& layoutSize = mModel->mVisualModel->GetLayoutSize(); - const Vector2 currentScrollPosition = mModel->mScrollPosition; - - mModel->mScrollPosition.x += xSpeed; - mModel->mScrollPosition.y += ySpeed; - - ClampHorizontalScroll( layoutSize ); - ClampVerticalScroll( layoutSize ); - - bool endOfScroll = false; - if( Vector2::ZERO == ( currentScrollPosition - mModel->mScrollPosition ) ) - { - // Notify the decorator there is no more text to scroll. - // The decorator won't send more scroll events. - mEventData->mDecorator->NotifyEndOfScroll(); - // Still need to set the position of the handle. - endOfScroll = true; - } - - // Set the position of the handle. - const bool scrollRightDirection = xSpeed > 0.f; - const bool scrollBottomDirection = ySpeed > 0.f; - const bool leftSelectionHandleEvent = Event::LEFT_SELECTION_HANDLE_EVENT == event.type; - const bool rightSelectionHandleEvent = Event::RIGHT_SELECTION_HANDLE_EVENT == event.type; - - if( Event::GRAB_HANDLE_EVENT == event.type ) - { - ChangeState( EventData::GRAB_HANDLE_PANNING ); - - // Get the grab handle position in decorator coords. - Vector2 position = mEventData->mDecorator->GetPosition( GRAB_HANDLE ); - - if( mEventData->mDecorator->IsHorizontalScrollEnabled() ) - { - // Position the grag handle close to either the left or right edge. - position.x = scrollRightDirection ? 0.f : mModel->mVisualModel->mControlSize.width; - } - - if( mEventData->mDecorator->IsVerticalScrollEnabled() ) - { - position.x = mEventData->mCursorHookPositionX; - - // Position the grag handle close to either the top or bottom edge. - position.y = scrollBottomDirection ? 0.f : mModel->mVisualModel->mControlSize.height; - } - - // Get the new handle position. - // The grab handle's position is in decorator's coords. Need to transforms to text's coords. - bool matchedCharacter = false; - const CharacterIndex handlePosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - position.x - mModel->mScrollPosition.x, - position.y - mModel->mScrollPosition.y, - CharacterHitTest::SCROLL, - matchedCharacter ); - - if( mEventData->mPrimaryCursorPosition != handlePosition ) - { - mEventData->mUpdateCursorPosition = true; - mEventData->mUpdateGrabHandlePosition = !isSmoothHandlePanEnabled; - mEventData->mScrollAfterUpdatePosition = true; - mEventData->mPrimaryCursorPosition = handlePosition; - } - mEventData->mUpdateInputStyle = mEventData->mUpdateCursorPosition; - - // Updates the decorator if the soft handle panning is enabled. - mEventData->mDecoratorUpdated = isSmoothHandlePanEnabled; - } - else if( leftSelectionHandleEvent || rightSelectionHandleEvent ) - { - ChangeState( EventData::SELECTION_HANDLE_PANNING ); - - // Get the selection handle position in decorator coords. - Vector2 position = mEventData->mDecorator->GetPosition( leftSelectionHandleEvent ? Text::LEFT_SELECTION_HANDLE : Text::RIGHT_SELECTION_HANDLE ); - - if( mEventData->mDecorator->IsHorizontalScrollEnabled() ) - { - // Position the selection handle close to either the left or right edge. - position.x = scrollRightDirection ? 0.f : mModel->mVisualModel->mControlSize.width; - } - - if( mEventData->mDecorator->IsVerticalScrollEnabled() ) - { - position.x = mEventData->mCursorHookPositionX; - - // Position the grag handle close to either the top or bottom edge. - position.y = scrollBottomDirection ? 0.f : mModel->mVisualModel->mControlSize.height; - } - - // Get the new handle position. - // The selection handle's position is in decorator's coords. Need to transform to text's coords. - bool matchedCharacter = false; - const CharacterIndex handlePosition = Text::GetClosestCursorIndex( mModel->mVisualModel, - mModel->mLogicalModel, - mMetrics, - position.x - mModel->mScrollPosition.x, - position.y - mModel->mScrollPosition.y, - CharacterHitTest::SCROLL, - matchedCharacter ); - - if( leftSelectionHandleEvent ) - { - const bool differentHandles = ( mEventData->mLeftSelectionPosition != handlePosition ) && ( mEventData->mRightSelectionPosition != handlePosition ); - - if( differentHandles || endOfScroll ) - { - mEventData->mUpdateHighlightBox = true; - mEventData->mUpdateLeftSelectionPosition = !isSmoothHandlePanEnabled; - mEventData->mUpdateRightSelectionPosition = isSmoothHandlePanEnabled; - mEventData->mLeftSelectionPosition = handlePosition; - } - } - else - { - const bool differentHandles = ( mEventData->mRightSelectionPosition != handlePosition ) && ( mEventData->mLeftSelectionPosition != handlePosition ); - if( differentHandles || endOfScroll ) - { - mEventData->mUpdateHighlightBox = true; - mEventData->mUpdateRightSelectionPosition = !isSmoothHandlePanEnabled; - mEventData->mUpdateLeftSelectionPosition = isSmoothHandlePanEnabled; - mEventData->mRightSelectionPosition = handlePosition; - } - } - - if( mEventData->mUpdateLeftSelectionPosition || mEventData->mUpdateRightSelectionPosition ) - { - RepositionSelectionHandles(); - - mEventData->mScrollAfterUpdatePosition = !isSmoothHandlePanEnabled; - } - } - mEventData->mDecoratorUpdated = true; - } // end ( HANDLE_SCROLLING == state ) + ControllerImplEventHandler::OnHandleEvent(*this, event); } void Controller::Impl::OnSelectEvent( const Event& event ) { - if( NULL == mEventData ) - { - // Nothing to do if there is no text. - return; - } - - if( mEventData->mSelectionEnabled ) - { - // Convert from control's coords to text's coords. - const float xPosition = event.p2.mFloat - mModel->mScrollPosition.x; - const float yPosition = event.p3.mFloat - mModel->mScrollPosition.y; - - // Calculates the logical position from the x,y coords. - RepositionSelectionHandles( xPosition, - yPosition, - Controller::NoTextTap::HIGHLIGHT ); - } + ControllerImplEventHandler::OnSelectEvent(*this, event); } void Controller::Impl::OnSelectAllEvent() { - DALI_LOG_INFO( gLogFilter, Debug::Verbose, "OnSelectAllEvent mEventData->mSelectionEnabled%s \n", mEventData->mSelectionEnabled?"true":"false"); - - if( NULL == mEventData ) - { - // Nothing to do if there is no text. - return; - } - - if( mEventData->mSelectionEnabled ) - { - // Calculates the logical position from the start. - RepositionSelectionHandles( 0.f - mModel->mScrollPosition.x, - 0.f - mModel->mScrollPosition.y, - Controller::NoTextTap::HIGHLIGHT ); - - mEventData->mLeftSelectionPosition = 0u; - mEventData->mRightSelectionPosition = mModel->mLogicalModel->mText.Count(); - } + ControllerImplEventHandler::OnSelectAllEvent(*this); } void Controller::Impl::OnSelectNoneEvent() { - DALI_LOG_INFO( gLogFilter, Debug::Verbose, "OnSelectNoneEvent mEventData->mSelectionEnabled%s \n", mEventData->mSelectionEnabled?"true":"false"); - - if( NULL == mEventData ) - { - // Nothing to do if there is no text. - return; - } - - if( mEventData->mSelectionEnabled && mEventData->mState == EventData::SELECTING) - { - mEventData->mPrimaryCursorPosition = 0u; - mEventData->mLeftSelectionPosition = mEventData->mRightSelectionPosition = mEventData->mPrimaryCursorPosition; - ChangeState( EventData::INACTIVE ); - mEventData->mUpdateCursorPosition = true; - mEventData->mUpdateInputStyle = true; - mEventData->mScrollAfterUpdatePosition = true; - } + ControllerImplEventHandler::OnSelectNoneEvent(*this); } void Controller::Impl::SetTextSelectionRange(const uint32_t *pStart, const uint32_t *pEnd) @@ -2072,6 +1410,8 @@ void Controller::Impl::SetTextSelectionRange(const uint32_t *pStart, const uint3 { ChangeState( EventData::SELECTING ); mEventData->mUpdateHighlightBox = true; + mEventData->mUpdateLeftSelectionPosition = true; + mEventData->mUpdateRightSelectionPosition = true; } } } diff --git a/dali-toolkit/internal/text/text-controller-impl.h b/dali-toolkit/internal/text/text-controller-impl.h index 3fc33b3..fc30d7b 100755 --- a/dali-toolkit/internal/text/text-controller-impl.h +++ b/dali-toolkit/internal/text/text-controller-impl.h @@ -2,7 +2,7 @@ #define DALI_TOOLKIT_TEXT_CONTROLLER_IMPL_H /* - * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * Copyright (c) 2020 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ const float DEFAULT_TEXTFIT_STEP = 1.f; //Forward declarations struct CursorInfo; struct FontDefaults; +struct ControllerImplEventHandler; class SelectableControlInterface; @@ -823,6 +824,9 @@ public: float mTextFitMaxSize; ///< Maximum Font Size for text fit. Default 100 float mTextFitStepSize; ///< Step Size for font intervalse. Default 1 bool mTextFitEnabled : 1; ///< Whether the text's fit is enabled. + +private: + friend ControllerImplEventHandler; }; } // namespace Text diff --git a/dali-toolkit/internal/visuals/transition-data-impl.cpp b/dali-toolkit/internal/visuals/transition-data-impl.cpp index 78dab18..bda70a4b 100644 --- a/dali-toolkit/internal/visuals/transition-data-impl.cpp +++ b/dali-toolkit/internal/visuals/transition-data-impl.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 Samsung Electronics Co., Ltd. + * Copyright (c) 2020 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,114 @@ namespace Toolkit namespace Internal { +namespace +{ + +/// Parses a Property::Array and sets up the animator appropriately +void ParseArray(TransitionData::Animator* animator, const Property::Array* array) +{ + bool valid = true; + Vector4 controlPoints; + if( array && array->Count() >= 4 ) + { + for( size_t vecIdx = 0; vecIdx < 4; ++vecIdx ) + { + const Property::Value& v = array->GetElementAt(vecIdx); + if( v.GetType() == Property::FLOAT ) + { + controlPoints[vecIdx] = v.Get(); + } + else + { + valid = false; + break; + } + } + } + else + { + valid = false; + } + + if( valid ) + { + Vector2 controlPoint1( controlPoints.x, controlPoints.y ); + Vector2 controlPoint2( controlPoints.z, controlPoints.w ); + animator->alphaFunction = AlphaFunction( controlPoint1, controlPoint2 ); + } + else + { + animator->animate = false; + } +} + +/// Parses a string value and sets up the animator appropriately +void ParseString(TransitionData::Animator* animator, std::string alphaFunctionValue) +{ + if( alphaFunctionValue == "LINEAR" ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::LINEAR); + } + else if( ! alphaFunctionValue.compare(0, 5, "EASE_" ) ) + { + if( alphaFunctionValue == "EASE_IN" ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN); + } + else if( alphaFunctionValue == "EASE_OUT" ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT); + } + else if( ! alphaFunctionValue.compare( 5, 3, "IN_" ) ) + { + if( ! alphaFunctionValue.compare(8, -1, "SQUARE" )) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_SQUARE); + } + else if( ! alphaFunctionValue.compare(8, -1, "OUT" )) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_OUT); + } + else if( ! alphaFunctionValue.compare(8, -1, "OUT_SINE" )) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_OUT_SINE); + } + else if( ! alphaFunctionValue.compare(8, -1, "SINE" )) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_SINE); + } + } + else if( ! alphaFunctionValue.compare( 5, 4, "OUT_" ) ) + { + if( ! alphaFunctionValue.compare(9, -1, "SQUARE" ) ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT_SQUARE); + } + else if( ! alphaFunctionValue.compare(9, -1, "SINE" ) ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT_SINE); + } + else if( ! alphaFunctionValue.compare(9, -1, "BACK" ) ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT_BACK); + } + } + } + else if( alphaFunctionValue == "REVERSE" ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::REVERSE); + } + else if( alphaFunctionValue == "BOUNCE" ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::BOUNCE); + } + else if( alphaFunctionValue == "SIN" ) + { + animator->alphaFunction = AlphaFunction(AlphaFunction::SIN); + } +} +} // unnamed namespace + TransitionData::TransitionData() { } @@ -171,40 +279,7 @@ TransitionData::Animator* TransitionData::ConvertMap( const Property::Map& map) { if( value.GetType() == Property::ARRAY ) { - bool valid = true; - Vector4 controlPoints; - const Property::Array* array = value.GetArray(); - if( array && array->Count() >= 4 ) - { - for( size_t vecIdx = 0; vecIdx < 4; ++vecIdx ) - { - const Property::Value& v = array->GetElementAt(vecIdx); - if( v.GetType() == Property::FLOAT ) - { - controlPoints[vecIdx] = v.Get(); - } - else - { - valid = false; - break; - } - } - } - else - { - valid = false; - } - - if( valid ) - { - Vector2 controlPoint1( controlPoints.x, controlPoints.y ); - Vector2 controlPoint2( controlPoints.z, controlPoints.w ); - animator->alphaFunction = AlphaFunction( controlPoint1, controlPoint2 ); - } - else - { - animator->animate = false; - } + ParseArray(animator, value.GetArray()); } else if( value.GetType() == Property::VECTOR4 ) { @@ -215,69 +290,7 @@ TransitionData::Animator* TransitionData::ConvertMap( const Property::Map& map) } else if( value.GetType() == Property::STRING ) { - std::string alphaFunctionValue = value.Get< std::string >(); - - if( alphaFunctionValue == "LINEAR" ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::LINEAR); - } - else if( ! alphaFunctionValue.compare(0, 5, "EASE_" ) ) - { - if( alphaFunctionValue == "EASE_IN" ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN); - } - else if( alphaFunctionValue == "EASE_OUT" ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT); - } - else if( ! alphaFunctionValue.compare( 5, 3, "IN_" ) ) - { - if( ! alphaFunctionValue.compare(8, -1, "SQUARE" )) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_SQUARE); - } - else if( ! alphaFunctionValue.compare(8, -1, "OUT" )) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_OUT); - } - else if( ! alphaFunctionValue.compare(8, -1, "OUT_SINE" )) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_OUT_SINE); - } - else if( ! alphaFunctionValue.compare(8, -1, "SINE" )) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_IN_SINE); - } - } - else if( ! alphaFunctionValue.compare( 5, 4, "OUT_" ) ) - { - if( ! alphaFunctionValue.compare(9, -1, "SQUARE" ) ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT_SQUARE); - } - else if( ! alphaFunctionValue.compare(9, -1, "SINE" ) ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT_SINE); - } - else if( ! alphaFunctionValue.compare(9, -1, "BACK" ) ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::EASE_OUT_BACK); - } - } - } - else if( alphaFunctionValue == "REVERSE" ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::REVERSE); - } - else if( alphaFunctionValue == "BOUNCE" ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::BOUNCE); - } - else if( alphaFunctionValue == "SIN" ) - { - animator->alphaFunction = AlphaFunction(AlphaFunction::SIN); - } + ParseString(animator, value.Get< std::string >()); } else { diff --git a/dali-toolkit/public-api/dali-toolkit-version.cpp b/dali-toolkit/public-api/dali-toolkit-version.cpp index b7dc35f..b9a4da5 100644 --- a/dali-toolkit/public-api/dali-toolkit-version.cpp +++ b/dali-toolkit/public-api/dali-toolkit-version.cpp @@ -29,7 +29,7 @@ namespace Toolkit { const unsigned int TOOLKIT_MAJOR_VERSION = 1; const unsigned int TOOLKIT_MINOR_VERSION = 9; -const unsigned int TOOLKIT_MICRO_VERSION = 34; +const unsigned int TOOLKIT_MICRO_VERSION = 35; const char* const TOOLKIT_BUILD_DATE = __DATE__ " " __TIME__; #ifdef DEBUG_ENABLED diff --git a/packaging/dali-toolkit.spec b/packaging/dali-toolkit.spec index 482b4d9..8023790 100644 --- a/packaging/dali-toolkit.spec +++ b/packaging/dali-toolkit.spec @@ -1,6 +1,6 @@ Name: dali2-toolkit Summary: Dali 3D engine Toolkit -Version: 1.9.34 +Version: 1.9.35 Release: 1 Group: System/Libraries License: Apache-2.0 and BSD-3-Clause and MIT