chromiumのタブをいじる(4)

この記事は東京大学電子情報工学科3年の後期実験である「大規模ソフトウェアを手探る」で行われた班活動を記事にしたものです．
以下の記事の続きになっています．(リンクは諸事情によりまだつながっていないです)
[chromiumのタブをいじる(１)](https://qiita.com/kons_9/items/9a9e3bf268a6e97f7e5a)
(1)でいじったタブが固定タブやタブグループに対応していないです．
これを対応させるのが今回の目的です．

## 実行環境
Ubuntu 18.04.5 (LTS)

## タブグループ，固定タブとは何か
固定タブは知っていてもタブグループって何だという方もいると思います．タブグループは割と新しい機能でその名の通りタブをグループ化できます．
図で見てもらった方が早いと思うので発表用スライドからの抜粋です．(ピン留めは固定タブのことです)
![発表用スライドの一部](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598781/a6077a39-68c2-85a0-0915-030acb430c1f.png)

## 問題点の整理
タブグループ，固定タブにわけて考えていきます．
#### 固定タブの問題点
固定タブは本来小さくなるはずですがこのままだと通常のタブと同様に扱ってしまい，大きくなってしまいます．

#### タブグループの問題点
こちらの方が闇が深いです．大きく分けて3点の問題点があります．
1つ目は非表示のタブが表示されてしまうことです．タブグループではグループ化して非表示にしたり表示にしたりすることによって，その有効性が発揮できると思うのですが，今の状態ではそれができません．
2つ目はactivetabがずれてしまうことです．アクティブタブがずれるというのは，今見ているタブじゃないところのタブが大きくなってしまっているということです．
3つ目は2つ目と関連しているのですがタブグループのヘッダ(タブグループの名前を保存している場所)が異常に大きくなってしまっていたりすることです．

## 原因推定
#### タブグループ2番目の問題点に関して
タブグループの2番目の原因は少し複雑な事情がありそうです．そこでタブグループがある状態でプログラムの挙動を調べていこうと思います．
しかし，これはおそらくPCのスペックの問題だと思うのですが，グループ化した場合，適当な場所にbreakpointを張ってデバッグしようとするとフリーズします．
そこでgdbデバッグを諦めコンソールのログから状態を確認することにします．具体的な方法としては

```cpp=
LOG(INFO)<<変数;
```
の形でデバッグしたい箇所に入れ目的の変数を入れれば大丈夫です．
まずタブの数を確認します．タブの数はbounds.size()でわかるのでこれをみると，普通のタブだけでなくグループヘッダを含めた個数になっていることがわかります．

対してactive_tab_model_indexはタブのヘッダを含めないようなタブだけを対象に左から何番目かを記録しているとわかります．これを使うと確かに左にあるタブグループヘッダの個数分だけactive_indexがずれてしまうとわかります．
ここでactive_tab_slot_indexを見てみるとこれはタブヘッダを含めたタブを対象に左から何番目かを表しているとわかります．

よって前回の実装をactive_tab_model_indexからactive_tab_slot_indexに変えればactiveタブの挙動は正常なものになると考えられます．
実際に既存のコードを模倣して

```cpp
...
  const int active_tab_model_index = controller_->GetActiveIndex();
  const int active_tab_slot_index =
      controller_->IsValidIndex(active_tab_model_index)
          ? GetSlotIndexForTabModelIndex(
                active_tab_model_index,
                tabs->view_at(active_tab_model_index)->group())
          : TabStripModel::kNoTab;
...

```
としてactive_tab_model_indexの場所をactive_tab_slot_indexに書き換えるとうまくいきます．

### 他のバグに関して
固定タブの原因とタブグループの原因1,3は明らかでboundsに入っている全てのオブジェクトに対して同様の計算式をしてしまっていることです．つまり今相手にしているオブジェクトが固定タブなのか，グループタブのヘッダなのか，グループの非表示タブなのかと言ったことがわかれば良いです．
どのようにして既存のコードが考えているのかを探ります．


#### 固定タブについて
まずは固定タブについてです．固定タブは
https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/tabs/tab_strip_layout_helper.cc;l=304;drc=929e790ad17a54008b3dac86cbe30751c968c2ce;bpv=1;bpt=1

```cpp
std::vector<gfx::Rect> TabStripLayoutHelper::CalculateIdealBounds(
...
  const int pinned_tab_count = GetPinnedTabCount();
  const int last_pinned_tab_index = pinned_tab_count - 1;
  const int last_pinned_tab_slot_index =
      pinned_tab_count > 0 ? GetSlotIndexForTabModelIndex(
                                 last_pinned_tab_index,
                                 tabs->view_at(last_pinned_tab_index)->group())
                           : TabStripModel::kNoTab;
...
  for (int i = 0; i < int{slots_.size()}; i++) {
    auto active =
        i == active_tab_slot_index ? TabActive::kActive : TabActive::kInactive;
    auto pinned = i <= last_pinned_tab_slot_index ? TabPinned::kPinned
                                                  : TabPinned::kUnpinned;
...
```
この辺で定義されるものが関係しそうで，特に最後のauto pinned...をみるとわかるように必ず左側に固まって存在することがわかります．
このことから固定タブの実装では個数のみを保管しておけば良いことがわかります．

#### 非表示のグループタブについて
グループタブが非表示か非表示じゃないか関係がありそうなのが固定タブのすぐ下にあり

```cpp
std::vector<gfx::Rect> TabStripLayoutHelper::CalculateIdealBounds(
...
    // The slot can only be collapsed if it is a tab and in a collapsed group.
    // If the slot is indeed a tab and in a group, check the collapsed state of
    // the group to determine if it is collapsed.
    // A collapsed tab animates close like a closed tab.
    base::Optional<tab_groups::TabGroupId> id = slots_[i].view->group();
    bool slot_is_collapsed_tab =
        (slots_[i].type == ViewType::kTab && id.has_value())
            ? controller_->IsGroupCollapsed(id.value())
            : false;
...
```
コメントを読むとわかるとおりこの辺が関係してきそうです．確かにslot_is_collapsed_tabがTrueとなっているタブを見てみると非表示のグループタブの中に入っていることがわかると思います．

#### グループヘッダについて
グループヘッダは一つ前の関数であるUpdateIdealBoundsにそれっぽいものがあります
https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/tabs/tab_strip_layout_helper.cc;l=242;drc=929e790ad17a54008b3dac86cbe30751c968c2ce;bpv=1;bpt=1

```cpp
int TabStripLayoutHelper::UpdateIdealBounds(int available_width) {
...
  for (int i = 0; i < int{bounds.size()}; ++i) {
    const TabSlot& slot = slots_[i];
    switch (slot.type) {
      case ViewType::kTab:
        if (!slot.animation->IsClosing()) {
          tabs->set_ideal_bounds(current_tab_model_index, bounds[i]);
          UpdateCachedTabWidth(i, bounds[i].width(),
                               i == active_tab_slot_index);
          ++current_tab_model_index;
        }
        break;
      case ViewType::kGroupHeader:
        if (!slot.view->dragging())
          group_header_ideal_bounds_[slot.view->group().value()] = bounds[i];
        break;
    }
...
```
このViewType::kGroupHeaderがそれっぽいです．実際に確認してみるとこれが呼ばれているindexがグループヘッダであることがわかります．

### 一度まとめ
1. ピンの情報は個数だけで良い
2. グループヘッダはslot_[i].typeがViewType::kGroupHeaderであるか確認すれば良い
3. 非表示かどうかはslot_からgroupIDを取得して，それがIsGroupCollapsedであるかどうかを見れば良い

ということがわかります．
これをどのように実装するのかですが，グループヘッダの扱いが少し難しそう(文字列に応じた大きさ制御)だったのと単純にこの段階で発表前の最終日だったので既存の関数にラッピングするような形で実現しました．
つまり，

1. 既存の関数が作ったboundsに対して，固定タブ，グループヘッダのwidthの合計を取得
2. 非表示タブの個数を取得
3. それらをもとにavailable_widthを分配し，表示タブをの大きさを再構成する

という手順を取りました．
実際のコードは以下です．効率化せずに書いたので長い反面,やっていること自体はわかりやすいのではないかと思います．


```cpp
std::vector<gfx::Rect> TabStripLayoutHelper::wrapping(int available_width,int size,std::vector<gfx::Rect> pre_bounds){
  views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
  
  const int active_tab_model_index = controller_->GetActiveIndex();
  const int active_tab_slot_index =
      controller_->IsValidIndex(active_tab_model_index)
          ? GetSlotIndexForTabModelIndex(
                active_tab_model_index,
                tabs->view_at(active_tab_model_index)->group())
          : TabStripModel::kNoTab;

  const int pinned_tab_count = GetPinnedTabCount();
  
  //今現在activetabがpinnedタブなら変更する必要はないのでそのままreturnする
  if(active_tab_slot_index<pinned_tab_count)
    return pre_bounds;

  std::vector<gfx::Rect> bounds;
  
  //maxのwidthを考える
  const int standard_width = TabStyle::GetStandardWidth();
  const TabLayoutConstants layout_constants = GetTabLayoutConstants();

  int overlap = layout_constants.tab_overlap;

  //pinnedタブの大きさは変えずに実行する
  for(int i=0;i<pinned_tab_count;i++){
    bounds.push_back(pre_bounds.at(i));
  }

  //pinnedタブを除いて考える
  size -= pinned_tab_count;

  //pinnedタブがあればそれに応じてavailable_widthを減らす．重なりに注意
  available_width += pinned_tab_count<=0?0:-pre_bounds.at(pinned_tab_count-1).right()+overlap;

  //現在のサイズをloop用に保管する
  int tmpsize = size;

  for(int i=pinned_tab_count;i<tmpsize+pinned_tab_count;i++){
    const TabSlot& slot = slots_[i];
    switch(slot.type){
      case ViewType::kTab:{
        //通常タブならそれが非表示かどうかを判定し非表示ならサイズを減らす
        base::Optional<tab_groups::TabGroupId> id = slot.view->group();
        if(id.has_value()&&controller_->IsGroupCollapsed(id.value()))
          size--;
        break;
      }
      case ViewType::kGroupHeader:{
        //groupヘッダならavailablesizeとsizeを減らす
        available_width-=pre_bounds[i].width();
        size--;
        break;}
    }
  }

  //size of active_tab is twice as normal
  int width = available_width+(size-1)*overlap;
  int common_width =(width/(size+1));
  int res = width%(size+1);

  //初期値もpinnedタブの個数に応じて変える
  int next_x=(pinned_tab_count<=0)?0:(pre_bounds.at(pinned_tab_count-1).right()-overlap);

  for(int i=pinned_tab_count;i<tmpsize+pinned_tab_count;i++){
    
    const TabSlot& slot = slots_[i];
    
    switch(slot.type){
      case ViewType::kTab:{
        //通常タブならpush_backする
        base::Optional<tab_groups::TabGroupId> id = slot.view->group();
        if(id.has_value()&&controller_->IsGroupCollapsed(id.value())){
          bounds.push_back(gfx::Rect(next_x,0,pre_bounds[i].width(),layout_constants.tab_height));
          break;}
        int tab_width;
        if(res>0){
          res--;
          tab_width=common_width+1;
        }else{
          tab_width = common_width;
        }
        if(i==active_tab_slot_index)
          tab_width+=common_width;
        
        if(tab_width>standard_width){
          tab_width = standard_width;
        }

        bounds.push_back(
          gfx::Rect(next_x,0,tab_width,layout_constants.tab_height)
        );
        next_x += tab_width-overlap;
        break;}
      case ViewType::kGroupHeader:{
       //グループヘッダはそのまま
       bounds.push_back(gfx::Rect(next_x,0,pre_bounds[i].width(),layout_constants.tab_height));
　　　　　
        next_x += pre_bounds[i].width()-overlap;
        break;
      }
    }
  }

  return bounds;
}
//end

int TabStripLayoutHelper::UpdateIdealBounds(int available_width) {
  //delete 
  //const std::vector<gfx::Rect> bounds = CalculateIdealBounds(available_width);
  //end

  // add 
  std::vector<gfx::Rect> bounds = CalculateIdealBounds(available_width);
  bounds =wrapping(available_width,bounds.size(),bounds);

```
このような感じでうまくいきます
実際のデモ動画です．
固定->固定->グループ化->グループ化->非表示...みたいな感じで進んでいきます

![hello.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598781/9ab3d99a-c005-a888-cd6c-d80051fe6f4f.gif)

のようになっていると思います．

### 感想
slotが何のためにあるのかわからなかったのがようやくわかってよかった
フリーズさせた回数は20回くらいかな？？
いや，chromiumデカすぎだよ...おい...

### 別のテーマ
この変更をショートカットキーを用いて実現します．
[chromiumのタブをいじる(2)](https://qiita.com/Hideyu/items/4a02531b7f7ce871fbcd)

この機能を拡張し直近でactiveになったタブをn個保存しておきそれを大きくします．
[chromiumのタブをいじる(3)](https://qiita.com/momorrow/items/dc2a1611dfcbea51e56b)