Chromiumを手探った#4 - タブページを可変にしよう

#3はこちら

ソースコードを書き換え、タブページを自在に変更できるようにしていきます。

新しいタブで開くページの変更

問題の「新しいタブを開く」という行為を司る、.../src/chrome/browser/ui/browser_tabstrip.ccなるファイルが(不撓不屈のgrepによって)発見されました。この中のAddTabat()関数によってタブページが開かれるので、あとはこの関数のURLを書き換えるだけです。

  • .../src/chrome/browser/ui/browser_tabstrip.cc
...

void AddTabAt(Browser* browser, const GURL& url, int idx, bool foreground) {
  // Time new tab page creation time.  We keep track of the timing data in
  // WebContents, but we want to include the time it takes to create the
  // WebContents object too.
  base::TimeTicks new_tab_start_time = base::TimeTicks::Now();
  chrome::NavigateParams params(browser,
      url.is_empty() ? GURL(chrome::kChromeUINewTabURL) : url,
      ui::PAGE_TRANSITION_TYPED);
  params.disposition = foreground ? WindowOpenDisposition::NEW_FOREGROUND_TAB
                                  : WindowOpenDisposition::NEW_BACKGROUND_TAB;
  params.tabstrip_index = idx;
  chrome::Navigate(&params);
  CoreTabHelper* core_tab_helper =
      CoreTabHelper::FromWebContents(params.target_contents);
  core_tab_helper->set_new_tab_start_time(new_tab_start_time);
}

...

これを次のように書き換えます。

...

// includeするファイルの追加
#include "components/prefs/pref_service.h"
#include "chrome/common/pref_names.h"

...

void AddTabAt(Browser* browser, const GURL& url, int idx, bool foreground) {

  // 追加ここから
  PrefService* prefs = browser->profile()->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kHomePage));
  // 追加ここまで

  // Time new tab page creation time.  We keep track of the timing data in
  // WebContents, but we want to include the time it takes to create the
  // WebContents object too.
  base::TimeTicks new_tab_start_time = base::TimeTicks::Now();
  chrome::NavigateParams params(browser,
      url.is_empty() ? new_dedede : url, // GURL(chrome::kChromeUINewTabURL)をnew_dededeに
      ui::PAGE_TRANSITION_TYPED);
  params.disposition = foreground ? WindowOpenDisposition::NEW_FOREGROUND_TAB
                                  : WindowOpenDisposition::NEW_BACKGROUND_TAB;
  params.tabstrip_index = idx;
  chrome::Navigate(&params);
  CoreTabHelper* core_tab_helper =
      CoreTabHelper::FromWebContents(params.target_contents);
  core_tab_helper->set_new_tab_start_time(new_tab_start_time);
}

...

ホームページの情報の取得方法は、.../src/chrome/browser/ui/views/toolbar/home_button.cc(ホームボタンの挙動に関するファイル)のOnPerformDrop()関数(ホームボタンが押された際に実行される)内でホームページ情報を取得している部分を参考にしました。
追加した#includeもhome_button.ccを参考に、preferenceに関するものです。


ソースコードを書き換えてビルドし直す(もう一度ninjaを実行する)ことで、変更点が適用されます。
これにより、新しいタブを開いた時に、ホームページとして登録されているURLを開くようになります。(これは設定から変更可能です)

問題点

改変したのがbrowser_tabstrip.ccのみなので、新しくChromiumを起動した時のページはkChromeUINewTabURLのままです。
つまり、設定画面でのタブページの変更が新しくChromiumを起動したとき(スタートアップページ)のみ適用されません。

スタートアップページもタブページに

起動時にページを開く挙動は、.../src/chrome/browser/ui/startup/startup_browser_creator_impl.cc内のAddStartupURLs()関数として定義されています。これをbrowser_tabstrip.ccと同じように書き換えれば良さそうです。

  • startup_browser_creator_impl.cc
...

void StartupBrowserCreatorImpl::AddStartupURLs(
    std::vector<GURL>* startup_urls) const {
  // If we have urls specified by the first run master preferences use them
  // and nothing else.
  if (browser_creator_ && startup_urls->empty()) {
    if (!browser_creator_->first_run_tabs_.empty()) {
      std::vector<GURL>::iterator it =
          browser_creator_->first_run_tabs_.begin();
      while (it != browser_creator_->first_run_tabs_.end()) {
        // Replace magic names for the actual urls.
        if (it->host() == "new_tab_page") {
          startup_urls->push_back(GURL(chrome::kChromeUINewTabURL));
        } else if (it->host() == "welcome_page") {
          startup_urls->push_back(internals::GetWelcomePageURL());
        } else {
          startup_urls->push_back(*it);
        }
        ++it;
      }
      browser_creator_->first_run_tabs_.clear();
    }
  }
  // Otherwise open at least the new tab page (and any pages deemed needed by
  // AddSpecialURLs()), or the set of URLs specified on the command line.
  if (startup_urls->empty()) {
    AddSpecialURLs(startup_urls);
    startup_urls->push_back(GURL(chrome::kChromeUINewTabURL));

    // Special case the FIRST_RUN_LAST_TAB case of the welcome page.
    if (welcome_run_type_ == WelcomeRunType::FIRST_RUN_LAST_TAB)
      startup_urls->push_back(internals::GetWelcomePageURL());
  }

  if (signin::ShouldShowPromoAtStartup(profile_, is_first_run_)) {
    signin::DidShowPromoAtStartup(profile_);

    const GURL sync_promo_url = signin::GetPromoURL(
        signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
        signin_metrics::Reason::REASON_SIGNIN_PRIMARY_ACCOUNT, false);

    // No need to add if the sync promo is already in the startup list.
    bool add_promo = true;
    for (std::vector<GURL>::const_iterator it = startup_urls->begin();
         it != startup_urls->end(); ++it) {
      if (*it == sync_promo_url) {
        add_promo = false;
        break;
      }
    }

    if (add_promo) {
      // If the first URL is the NTP, replace it with the sync promo. This
      // behavior is desired because completing or skipping the sync promo
      // causes a redirect to the NTP.
      if (!startup_urls->empty() &&
          startup_urls->at(0) == chrome::kChromeUINewTabURL) {
        startup_urls->at(0) = sync_promo_url;
      } else {
        startup_urls->insert(startup_urls->begin(), sync_promo_url);
      }
    }
  }
}

...

ところが、ここで問題が発生します。先ほどのAddTabat()関数は引数にBrowser* browserを取っていたため、browser->profile()->GetPrefs()を用いることができましたが、今回のAddStartupURLs()にはBrowser* browserが引数として指定されていません。
単純に引数を追加してもうまく行かず、ここだけは違う方法でGetPrefs()する必要があります。

ここではBrowser* browserは定義されていませんが、Profile* profile_が定義されているため、これを用いて、profile_->GetPrefs()とすると上手くいきました。以下が書き換えたあとのコードです。

...

void StartupBrowserCreatorImpl::AddStartupURLs(
    std::vector<GURL>* startup_urls) const {

  // 追加ここから
  PrefService* prefs = profile_->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kHomePage));
  // 追加ここまで

  // If we have urls specified by the first run master preferences use them
  // and nothing else.
  if (browser_creator_ && startup_urls->empty()) {
    if (!browser_creator_->first_run_tabs_.empty()) {
      std::vector<GURL>::iterator it =
          browser_creator_->first_run_tabs_.begin();
      while (it != browser_creator_->first_run_tabs_.end()) {
        // Replace magic names for the actual urls.
        if (it->host() == "new_tab_page") {
          startup_urls->push_back(new_dedede); // GURL(chrome::kChromeUINewTabURL)をnew_dededeに
        } else if (it->host() == "welcome_page") {
          startup_urls->push_back(internals::GetWelcomePageURL());
        } else {
          startup_urls->push_back(*it);
        }
        ++it;
      }
      browser_creator_->first_run_tabs_.clear();
    }
  }
  // Otherwise open at least the new tab page (and any pages deemed needed by
  // AddSpecialURLs()), or the set of URLs specified on the command line.
  if (startup_urls->empty()) {
    AddSpecialURLs(startup_urls);
    startup_urls->push_back(new_dedede)); // GURL(chrome::kChromeUINewTabURL)をnew_dededeに

    // Special case the FIRST_RUN_LAST_TAB case of the welcome page.
    if (welcome_run_type_ == WelcomeRunType::FIRST_RUN_LAST_TAB)
      startup_urls->push_back(internals::GetWelcomePageURL());
  }

  if (signin::ShouldShowPromoAtStartup(profile_, is_first_run_)) {
    signin::DidShowPromoAtStartup(profile_);

    const GURL sync_promo_url = signin::GetPromoURL(
        signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
        signin_metrics::Reason::REASON_SIGNIN_PRIMARY_ACCOUNT, false);

    // No need to add if the sync promo is already in the startup list.
    bool add_promo = true;
    for (std::vector<GURL>::const_iterator it = startup_urls->begin();
         it != startup_urls->end(); ++it) {
      if (*it == sync_promo_url) {
        add_promo = false;
        break;
      }
    }

    if (add_promo) {
      // If the first URL is the NTP, replace it with the sync promo. This
      // behavior is desired because completing or skipping the sync promo
      // causes a redirect to the NTP.
      if (!startup_urls->empty() &&
          startup_urls->at(0) == new_dedede) { // GURL(chrome::kChromeUINewTabURL)をnew_dededeに
        startup_urls->at(0) = sync_promo_url;
      } else {
        startup_urls->insert(startup_urls->begin(), sync_promo_url);
      }
    }
  }
}

...

こうして、Chromiumを立ち上げたり、新しいタブを開いたときのページを、設定画面から変更できるホームページとすることができました。

ホームページと新しいタブページを独立に

さて、ここまで見てきた方法では、タブページとホームボタンを押して開くホームページが必ず一致してしまいます。
これでは改善として不十分であろうということで、以下では設定画面のURL入力フォームを2つに増やし、片方をホームページ・片方をタブページとするために書き換えた部分を見ていきます。
様々なファイルに渡るため、変更後のコードの変更部分のみここでは紹介します。

  • .../src/chrome/browser/resources/options/home_page_overlay.html
...

    <h2>ホームページ</h2> <!-- わかりやすさのための追記 --> 
    <div class="radio controlled-setting-with-label">
      <label>
        <input id="homepage-use-ntp" type="radio" name="homepage"
            pref="homepage_is_newtabpage" value="true"
            metric="Options_Homepage_IsNewTabPage" dialog-pref>
        <span>
          <span i18n-content="homePageUseNewTab"></span>
          <span class="controlled-setting-indicator"
              pref="homepage_is_newtabpage" value="true" dialog-pref></span>
        </span>
      </label>
    </div>
    <div class="radio controlled-setting-with-label">
      <label class="option-name">
        <input id="homepage-use-url" type="radio" name="homepage"
            pref="homepage_is_newtabpage" value="false"
            metric="Options_Homepage_IsNewTabPage" dialog-pref>
        <span>
          <span id="homepage-use-url-label" i18n-content="homePageUseURL">
          </span>
          <span class="controlled-setting-indicator"
              pref="homepage_is_newtabpage" value="false" dialog-pref></span>
        </span>
      </label>
      <input id="homepage-url-field" type="url" data-type="url"
          class="weakrtl favicon-cell stretch" pref="homepage"
          aria-labelledby="homepage-use-url-label"
          metric="Options_Homepage_URL" dialog-pref>
      </input>
      <span id="homepage-url-field-indicator"
          class="controlled-setting-indicator" pref="homepage"
          dialog-pref>
      </span>
    </div>
<!-- 追加部分ここから -->
    <h2>新しいタブ</h2>
    <div class="radio controlled-setting-with-label">
      <label>
        <input id="dedede-use-ntp" type="radio" name="dedede"
            pref="dedede_is_default" value="true"
            metric="Options_Dedede_IsDefault" dialog-pref>
        <span>
          <span>デフォルトの新しいタブ</span>
          <span class="controlled-setting-indicator"
              pref="dedede_is_default" value="true" dialog-pref></span>
        </span>
      </label>
    </div>
    <div class="radio controlled-setting-with-label">
      <label class="option-name">
        <input id="dedede-use-url" type="radio" name="dedede"
            pref="dedede_is_default" value="false"
            metric="Options_Dedede_IsDefault" dialog-pref>
        <span>
          <span id="dedede-use-url-label" i18n-content="homePageUseURL">
          </span>
          <span class="controlled-setting-indicator"
              pref="dedede_is_default" value="false" dialog-pref></span>
        </span>
      </label>
      <input id="dedede-url-field" type="url" data-type="url"
          class="weakrtl favicon-cell stretch" pref="dedede"
          aria-labelledby="homepage-use-url-label"
          metric="Options_Dedede_URL" dialog-pref>
      </input>
      <span id="dedede-url-field-indicator"
          class="controlled-setting-indicator" pref="dedede"
          dialog-pref>
      </span>
    </div>
  </div>
<!-- 追加部分ここまで -->

...
  • .../src/chrome/browser/resources/options/home_page_overlay.js
...

      var self = this;
      options.Preferences.getInstance().addEventListener(
          'homepage_is_newtabpage',
          this.handleHomepageIsNTPPrefChange.bind(this));

      // 追加部分ここから
      options.Preferences.getInstance().addEventListener(
          'dedede_is_default',
          this.handleDededeIsDefaultPrefChange.bind(this));
      // 追加部分ここまで

      var urlField = $('homepage-url-field');
      urlField.addEventListener('keydown', function(event) {
        // Don't auto-submit when the user selects something from the
        // auto-complete list.
        if (event.key == 'Enter' && !self.autocompleteList_.hidden)
          event.stopPropagation();
      });
      urlField.addEventListener('change', this.updateFavicon_.bind(this));

      // 追加部分ここから
      var dededeField = $('dedede-url-field');
      dededeField.addEventListener('keydown', function(event) {
        // Don't auto-submit when the user selects something from the
        // auto-complete list.
        if (event.key == 'Enter' && !self.autocompleteList_.hidden)
          event.stopPropagation();
      });
      dededeField.addEventListener('change', this.updateFavicon_.bind(this));
      // 追加部分ここまで

...

    handleHomepageIsNTPPrefChange: function(event) {
      var urlField = $('homepage-url-field');
      var urlFieldIndicator = $('homepage-url-field-indicator');
      urlField.setDisabled('homepage-is-ntp', event.value.value);
      urlFieldIndicator.readOnly = event.value.value;
    },

    // 追加部分ここから
    handleDededeIsDefaultPrefChange: function(event) {
      var urlField = $('dedede-url-field');
      var urlFieldIndicator = $('dedede-url-field-indicator');
      urlField.setDisabled('dedede-is-default', event.value.value);
      urlFieldIndicator.readOnly = event.value.value;
    },
    // 追加部分ここまで

    /**
     * Updates the background of the url field to show the favicon for the
     * URL that is currently typed in.
     * @private
     */
    updateFavicon_: function() {
      var urlField = $('homepage-url-field');
      urlField.style.backgroundImage = cr.icon.getFavicon(urlField.value);

    // 追加部分ここから
      var dededeField = $('dedede-url-field');
      dededeField.style.backgroundImage = cr.icon.getFavicon(dededeField.value);
    // 追加部分ここまで
    },

...

    handleConfirm: function() {
      // Strip whitespace.
      var urlField = $('homepage-url-field');
      var homePageValue = urlField.value.replace(/\s*/g, '');
      urlField.value = homePageValue;

    // 追加部分ここから
      var dededeField = $('dedede-url-field');
      var dededePageValue = dededeField.value.replace(/\s*/g, '');
      dededeField.value = dededePageValue;
    // 追加部分ここまで

      // Don't save an empty URL for the home page. If the user left the field
      // empty, switch to the New Tab page.
      if (!homePageValue)
        $('homepage-use-ntp').checked = true;

    // 追加部分ここから
      if (!dededePageValue)
        $('dedede-use-ntp').checked = true;
    // 追加部分ここまで

      SettingsDialog.prototype.handleConfirm.call(this);
    },

...
  • .../src/chrome/common/pref_names.h
...

extern const char kHomePageIsNewTabPage[];
extern const char kHomePage[];

// 追加部分ここから
extern const char kDedede[];
extern const char kDededeIsDefault[];
// 追加部分ここまで
 
#if defined(OS_WIN)
extern const char kLastProfileResetTimestamp[];

...
  • .../src/chrome/common/pref_names.cc
...

// This is the URL of the page to load when opening new tabs.
const char kHomePage[] = "homepage";

// 追加部分ここから
const char kDedede[] = "dedede";
const char kDededeIsDefault[] = "dedede_is_default";
// 追加部分ここまで
  
#if defined(OS_WIN)

...
  • .../src/chrome/browser/ui/browser_tabstrip.cc
...

void AddTabAt(Browser* browser, const GURL& url, int idx, bool foreground) {

  PrefService* prefs = browser->profile()->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kDedede));

  // 追加部分ここから
  if(prefs->GetBoolean(prefs::kDededeIsDefault)){
    new_dedede = GURL(chrome::kChromeUINewTabURL);
  }
  // 追加部分ここまで

...
  • .../src/chrome/browser/ui/startup_browser_creator_impl.cc
...

void StartupBrowserCreatorImpl::AddStartupURLs(
    std::vector<GURL>* startup_urls) const {

  PrefService* prefs = profile_->GetPrefs();
  GURL new_dedede(prefs->GetString(prefs::kDedede));

  // 追加部分ここから
  if(prefs->GetBoolean(prefs::kDededeIsDefault)){
    new_dedede = GURL(chrome::kChromeUINewTabURL);
  }
  // 追加部分ここまで

...

ここまでひと通り書き換えて、終わった―!とビルドして実行するとコアダンします。(src/chrome/browser/ui/webui/options/core_options_handler.cc、376行目のNOTREACHED();において)
前後のコードを睨んだところ、どうやらpref_name(dedede, dedede_is_default)が認識されていないようです。
これについては、
www.chromium.org
を参考に、RegisterStringPref()という関数でpref_names.ccに宣言した変数を登録することで解決できます。
書き方や登録する場所を探すために"RegisterStringPref(prefs::kHomePage"でgrepすると、.../src/chrome/browser/profiles/profile_impl.ccで登録されていることが分かりました。

  • .../src/chrome/browser/profiles/profile_impl.cc
...

  registry->RegisterStringPref(prefs::kHomePage,
                               std::string(),
                               home_page_flags);
  
  // 追加部分ここから
  registry->RegisterStringPref(prefs::kDedede, std::string());
  registry->RegisterBooleanPref(prefs::kDededeIsDefault, true);
  // 追加部分ここまで
  
#if defined(ENABLE_PRINTING)

...

こんどこそ!とビルドし直すと、コアダンプすることはなくなり、一見所望の動作をしているかのように見えます。
が、ホームページの設定を"新しいタブを開く"にした状態でホームボタンを押すと設定した新しいタブのページではなくデフォルトの新しいタブ(url_constants.ccで定義されたもの)のページを開いてしまいました。
これを解決するためには、profile_impl.ccを更に弄る必要があります。

  • .../src/chrome/browser/profiles/profile_impl.cc
...

GURL ProfileImpl::GetHomePage() {
  // --homepage overrides any preferences.
  const base::CommandLine& command_line =
      *base::CommandLine::ForCurrentProcess();
  if (command_line.HasSwitch(switches::kHomePage)) {
    // TODO(evanm): clean up usage of DIR_CURRENT.
    //   http://code.google.com/p/chromium/issues/detail?id=60630
    // For now, allow this code to call getcwd().
    base::ThreadRestrictions::ScopedAllowIO allow_io;

    base::FilePath browser_directory;
    PathService::Get(base::DIR_CURRENT, &browser_directory);
    GURL home_page(url_formatter::FixupRelativeFile(
        browser_directory,
        command_line.GetSwitchValuePath(switches::kHomePage)));
    if (home_page.is_valid())
      return home_page;
  }

  // 追加部分ここから
  GURL new_dedede(GetPrefs()->GetString(prefs::kDedede));
  if(GetPrefs()->GetBoolean(prefs::kDededeIsDefault)){
    new_dedede = GURL(chrome::kChromeUINewTabURL);
  }
  // 追加部分ここまで

  if (GetPrefs()->GetBoolean(prefs::kHomePageIsNewTabPage))
    return new_dedede; // GURL(chrome::kChromeUINewTabURL);をnew_dededeに
  GURL home_page(url_formatter::FixupURL(
      GetPrefs()->GetString(prefs::kHomePage), std::string()));
  if (!home_page.is_valid())
    return new_dedede; // GURL(chrome::kChromeUINewTabURL);をnew_dededeに
  return home_page;
}

...

その後、src/chrome/browser/resources/options/browser_options.js(html)に加筆し表示を整えることもできます。

ここまで弄ってようやく、下画像のような設定ページにURLを入力し、ホームページとタブページを独立に設定することを実現できました!
f:id:iuias:20161028150827p:plain

おわりに

今回Chromiumを弄ってみて、ブラウザってこんなに大規模なソフトウェアだったんだな、と再確認させられました。
傍から見れば少しかもしれない機能変更をするのにこんなにもいろいろなファイルを書き換える必要があるなんて、気楽に構えていた実験開始時の自分を殴りたい気分です。
また、もう少し時間があれば、もっと(ネタ的な意味で)面白い追加機能を付けれたんじゃないかと、それだけが心残りです。

とはいえ、この実験で身につけたコードサーチの方法や、諦めない姿勢は非常に大切なものですし、終わってしまえばなかなか楽しい実験だったと思います。

この記事を読んでくださっている皆さんも、Chromium弄ってみましょう。自分好みのブラウザにしちゃいましょう!