OSDN Git Service

Inform to ThreadWindow that ResNum increases.
[fukui-no-namari/dialektos.git] / src / board_window.cxx
index b08036e..70bc837 100644 (file)
 
 #include "board_window.hxx"
 
+#include <glibmm/ustring.h>
+#include <glibmm/refptr.h>
+#include <sigc++/functors/mem_fun.h>
+#include <sigc++/signal.h>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/format.hpp>
+#include <boost/bind.hpp>
+#include <boost/foreach.hpp>
+#include <boost/mpl/find.hpp>
+
+#include <string>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+#include <vector>
+#include <algorithm>
+
 #include "bbs_detail_base.hxx"
+#include "thread_list_model.hxx"
+#include "board_view_column.hxx"
+#include "board_subject_item.hxx"
+#include "thread_idx_cache.hxx"
+#include "uri_opener.hxx"
+#include "http_get.hxx"
+#include "board_subject_idx.hxx"
+#include "http_date.hxx"
+#include "misc.hxx"
+#include "board_window_state.hxx"
+#include "thread_idx.hxx"
+#include "thread_window.hxx"
 
 
 namespace dialektos {
 
+namespace {
+struct LessSecond {
+  template <typename PairType>
+  bool operator()(const PairType& lhs, const PairType& rhs) const {
+    return lhs.second < rhs.second;
+  }
+};
+}
+
 
 void BoardWindow::create(std::auto_ptr<bbs_detail::Base> bbs) {
   regist(new BoardWindow(bbs));
 }
 
 BoardWindow::BoardWindow(std::auto_ptr<bbs_detail::Base> bbs) :
-  ApplicationFrameWork(), tree_view_(), bbs_(bbs) {
+  ApplicationFrameWork(), tree_view_(), bbs_(bbs), scrolled_(),
+  tree_model_(ThreadListModel::create()),
+  http_getter_()
+  {
+
+  // additional menuitems for board window
+  action_group_->add(Gtk::Action::create("FileOpen", "_Open Thread"),
+      sigc::mem_fun(*this, &BoardWindow::on_action_file_open));
+
+  Glib::ustring ui =
+    "<ui>"
+    "  <menubar name='MenuBar'>"
+    "    <menu action='MenuFile'>"
+    "      <menuitem action='FileOpen'/>"
+    "    </menu>"
+    "  </menubar>"
+    "  <popup name='MenuPopup'>"
+    "    <menuitem action='FileOpen'/>"
+    "  </popup>"
+    "</ui>";
+
+  ui_manager_->add_ui_from_string(ui);
+
+
+  tree_view_.signal_row_activated().connect(
+      sigc::mem_fun(*this, &BoardWindow::on_row_activated));
+  tree_view_.signal_button_press_event().connect_notify(
+      sigc::mem_fun(*this, &BoardWindow::on_child_button_press));
+
+
+  scrolled_.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+  add(scrolled_);
+  scrolled_.add(tree_view_);
+
+  tree_view_.set_model(tree_model_);
+  tree_view_.set_fixed_height_mode(true);
+  tree_view_.set_rules_hint(true);
+
+  BoardWindowState state;
+  state.from_xml(boost::filesystem::path(bbs_->get_board_state_path()));
+
+  add_column<view_column::Number>(state);
+  add_column<view_column::Title>(state);
+  add_column<view_column::ResNum>(state);
+  add_column<view_column::LineCount>(state);
+  add_column<view_column::Average>(state);
+  add_column<view_column::StartTime>(state);
+
+  set_default_size(state.width, state.height);
+  if (state.menubar) menubar_->show();
+  if (state.toolbar) toolbar_->show();
+  if (state.statusbar) statusbar_.show();
+
+  // set columns widths
+  BOOST_FOREACH(Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
+    view_column::Base* column =
+      dynamic_cast<view_column::Base*>(kolumn);
+
+    assert(column);
+    BoardWindowState::ColumnInfo info = state.columns[column->get_id()];
+    if (info.width > 0) column->set_fixed_width(info.width);
+  }
+
+  // set columns order.
+  typedef std::pair<Gtk::TreeViewColumn*, int> PairType;
+  std::vector<PairType> column_order;
+  BOOST_FOREACH(Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
+    view_column::Base* column =
+      dynamic_cast<view_column::Base*>(kolumn);
+
+    assert(column);
+    BoardWindowState::ColumnInfo info = state.columns[column->get_id()];
+    column_order.push_back(std::make_pair(column, info.order));
+  }
+  std::sort(column_order.begin(), column_order.end(), LessSecond());
+
+  Gtk::TreeViewColumn* previous_column = 0;
+  BOOST_FOREACH(PairType& pair, column_order) {
+    if (!previous_column) tree_view_.move_column_to_start(*pair.first);
+    else tree_view_.move_column_after(*pair.first, *previous_column);
+    previous_column = pair.first;
+  }
+
+  using namespace boost::posix_time;
+  ptime start = microsec_clock::local_time();
+
+  load();
 
-  set_default_size(400,300);
+  std::cout <<
+  to_simple_string(microsec_clock::local_time() - start) << std::endl;
+
+  set_title(get_uri());
 
-  add(tree_view_);
   show_all();
 }
 
 BoardWindow::~BoardWindow() {
 }
 
+void BoardWindow::on_informed_from(const bbs_detail::Base& bbs,
+    const ThreadIdx& idx) {
+  if (!bbs.belongs_to(*bbs_)) return;
+
+  ModelColumns cols;
+  model_column::field<model_column::Title>(cols) = idx.title_;
+  model_column::field<model_column::ID>(cols) = bbs.get_thread_id();
+  model_column::field<model_column::LineCount>(cols) = idx.line_count_;
+  tree_model_->update_row(cols);
+}
+
+void BoardWindow::on_row_activated(const Gtk::TreeModel::Path& path,
+    Gtk::TreeViewColumn* /*col*/) {
+  Gtk::TreeIter iter = tree_model_->get_iter(path);
+  std::string id;
+  iter->get_value(boost::mpl::find<model_column::List,
+      model_column::ID>::type::pos::value, id);
+
+  const std::string uri = bbs_->get_another_thread_uri(id);
+  uri_opener::open(uri);
+}
+
+void BoardWindow::on_action_view_stop() {
+  if (http_getter_) http_getter_->cancel();
+}
+
 bool BoardWindow::is_same(const bbs_detail::Base& bbs) const {
   return *bbs_ == bbs;
 }
 
+std::string BoardWindow::get_uri() const {
+  return bbs_->get_board_uri();
+}
+
+
+void BoardWindow::load() {
+  boost::unordered_map<model_column::ID::type, ModelColumns> buffer;
+
+  std::vector<ThreadIdxCache> caches;
+
+  const boost::filesystem::path dir(bbs_->get_board_idx_dir_path());
+  if (boost::filesystem::exists(dir) && boost::filesystem::is_directory(dir))
+    caches = dialektos::ThreadIdxCache::from_directory(dir);
+
+  BOOST_FOREACH(const ThreadIdxCache& cache, caches) {
+    ModelColumns cols;
+    model_column::field<model_column::Title>(cols) = cache.title_;
+    model_column::field<model_column::ID>(cols) = cache.id_;
+    model_column::field<model_column::LineCount>(cols) = cache.line_count_;
+    buffer[cache.id_] = cols;
+  }
+
+  const boost::filesystem::path sbj(bbs_->get_board_subject_path());
+  if (boost::filesystem::exists(sbj) && boost::filesystem::is_regular(sbj)) {
+    std::vector<SubjectItem> subjects;
+    bbs_->load_subject(subjects);
+
+    idx_ = SubjectIdx::from_xml(
+        boost::filesystem::path(bbs_->get_board_subject_idx_path()));
+
+    merge_subject_txt(subjects, buffer);
+  } else
+    on_action_view_refresh();
+
+  tree_model_->set_buffer(buffer);
+}
+
+void BoardWindow::merge_subject_txt(
+    const std::vector<SubjectItem>& subjects,
+    boost::unordered_map<std::string, ModelColumns>& buffer) {
+
+  boost::posix_time::ptime last_modified;
+  try {
+    if (!idx_.last_modified_.empty())
+      last_modified = rfc1123_to_ptime(idx_.last_modified_);
+    else
+      last_modified = boost::posix_time::second_clock::universal_time();
+  } catch (const HTTPDateError& e) {
+    std::cerr << e.what() << std::endl;
+    last_modified = boost::posix_time::second_clock::universal_time();
+  }
+
+  BOOST_FOREACH(const SubjectItem& item, subjects) {
+    ModelColumns& cols = buffer[item.id_];
+    model_column::field<model_column::Number>(cols) = item.number_;
+    model_column::field<model_column::Title>(cols) = item.title_;
+    model_column::field<model_column::ID>(cols) = item.id_;
+    model_column::field<model_column::ResNum>(cols) = item.res_num_;
+
+    // avoid some threads like '9242006007'
+    if (item.id_.size() == 10 && item.id_[0] == '9') continue;
+
+    try {
+      const std::time_t _start = boost::lexical_cast<std::time_t>(item.id_);
+      const boost::posix_time::ptime start = boost::posix_time::from_time_t(_start);
+      const boost::posix_time::ptime curr = last_modified;
+      const boost::posix_time::time_duration dur = curr - start;
+      const double sec = dur.total_seconds();
+      const double average = item.res_num_ / (sec/60.0/60.0/24.0);
+      model_column::field<model_column::Average>(cols) = average;
+    } catch(const boost::bad_lexical_cast& e) {
+      std::cerr << e.what() << "(" << item.id_ << ")" << std::endl;
+    }
+  }
+}
+
+void BoardWindow::unmerge_subject_txt(
+    boost::unordered_map<std::string, ModelColumns>& buffer) {
+  // remove thread ids which line_count_ is zero.
+  std::list<std::string> remove_list;
+  typedef boost::unordered_map<std::string, ModelColumns>::value_type PairType;
+  BOOST_FOREACH(PairType& pair, buffer) {
+    ModelColumns& cols = pair.second;
+    if (model_column::field<model_column::LineCount>(cols) == 0)
+      remove_list.push_back(pair.first);
+    else if (model_column::field<model_column::Number>(cols) != 0) {
+      model_column::field<model_column::Number>(cols) = 0;
+      model_column::field<model_column::ResNum>(cols) = 0;
+      model_column::field<model_column::Average>(cols) = 0;
+    }
+  }
+  BOOST_FOREACH(const std::string& id, remove_list)
+    buffer.erase(id);
+}
+
+void BoardWindow::on_action_file_open() {
+  std::cout << "file open activated" << std::endl;
+}
+
+void BoardWindow::on_action_edit_copy() {
+  Glib::RefPtr<const Gtk::TreeSelection> selection =
+    tree_view_.get_selection();
+
+  std::vector<Gtk::TreeModel::Path> paths = selection->get_selected_rows();
+  if (paths.empty()) return;
+
+  std::string selected;
+  BOOST_FOREACH(Gtk::TreeModel::Path path, paths) {
+    Gtk::TreeRow row = *(tree_model_->get_iter(path));
+    std::string id;
+    row.get_value(boost::mpl::find<model_column::List,
+        model_column::ID>::type::pos::value, id);
+    if (id.empty()) continue;
+    if (!selected.empty()) selected += "\n";
+    selected += bbs_->get_another_thread_uri(id);
+  }
+  Gtk::Clipboard::get()->set_text(selected);
+}
+
+void BoardWindow::on_action_view_refresh() {
+  if (http_getter_) return;
+
+  statusbar_.push("HTTP/1.0 GET...");
+
+  const std::string uri = bbs_->get_board_subject_uri();
+  http::Header request_header = bbs_->get_board_subject_request_header();
+
+  SubjectIdx idx = SubjectIdx::from_xml(
+      boost::filesystem::path(bbs_->get_board_subject_idx_path()));
+  request_header.set_if_modified_since(idx.last_modified_);
+
+  http_getter_.reset(new http::GetInThread(uri, request_header));
+  http_getter_->signal_end().connect(
+      sigc::mem_fun(*this, &BoardWindow::on_http_get_end));
+  http_getter_->run();
+}
+
+void BoardWindow::save_content(const http::Response& response) {
+  // save the content to the file subject.txt
+  if (!misc::create_directories(boost::filesystem::path(
+          bbs_->get_board_subject_path()).parent_path())) return;
+  std::ofstream ofs(bbs_->get_board_subject_path().c_str());
+  ofs << response.get_content();
+  ofs.close();
+
+  // save the metadata to the file subject.xml
+  idx_.last_modified_ = response.get_header().get_last_modified();
+  idx_.to_xml(boost::filesystem::path(bbs_->get_board_subject_idx_path()));
+}
+
+void BoardWindow::on_http_get_end(bool success) {
+//  const std::string uri = http_getter_->get_uri();
+//  const http::Header request_header = http_getter_->get_request_header();
+  const http::Response response = http_getter_->get_response();
+  const boost::system::error_code err = http_getter_->get_error();
+  http_getter_.reset(0);
+  if (err) {
+    statusbar_.push(err.message());
+    return;
+  }
+  if (!success) {
+    statusbar_.push("Canceled.");
+    return;
+  }
+
+  on_refresh_end(response.get_status_line(), response.get_header());
+
+  std::vector<SubjectItem> subjects;
+  bbs_->load_subject_from_string(response.get_content(), subjects);
+
+  if (subjects.empty()) return;
+
+  save_content(response);
+
+  boost::unordered_map<std::string, ModelColumns> buffer;
+  tree_model_->get_buffer(buffer);
+  unmerge_subject_txt(buffer);
+  merge_subject_txt(subjects, buffer);
+  tree_model_->set_buffer(buffer);
+
+  inform_to_threads(buffer);
+}
+
+void BoardWindow::on_refresh_end(const http::StatusLine& status,
+    const http::Header& header) {
+  std::string message = status.get_line();
+  const std::string last_modified = header.get_last_modified();
+  if (!last_modified.empty()) {
+    message += " ";
+    message += last_modified;
+  }
+  statusbar_.push(message);
+}
+
+void BoardWindow::save_state() const{
+  BoardWindowState state;
+  state.width = get_width();
+  state.height = get_height();
+  state.menubar = menubar_->is_visible();
+  state.toolbar = toolbar_->is_visible();
+  state.statusbar = statusbar_.is_visible();
+
+  int order = 0;
+  BOOST_FOREACH(const Gtk::TreeViewColumn* kolumn, tree_view_.get_columns()) {
+    const view_column::Base* column =
+      dynamic_cast<const view_column::Base*>(kolumn);
+
+    assert(column);
+
+    BoardWindowState::ColumnInfo info;
+    info.width = column->get_width();
+    info.order = order;
+    state.columns[column->get_id()] = info;
+    ++order;
+
+    if (column->get_sort_indicator()) {
+      state.sort.column = column->get_id();
+      state.sort.ascendant = column->get_sort_order() == Gtk::SORT_ASCENDING;
+    }
+  }
+
+  state.to_xml(boost::filesystem::path(bbs_->get_board_state_path()));
+}
+
+void BoardWindow::inform_to_threads(
+    boost::unordered_map<std::string, ModelColumns>& buffer) const {
+  typedef boost::unordered_map<std::string, ModelColumns> MapType;
+  typedef MapType::const_iterator Iter;
+
+  BOOST_FOREACH(ApplicationWindow& window, windows) {
+    if (ThreadWindow* thread_window = dynamic_cast<ThreadWindow*>(&window)) {
+      const bbs_detail::Base& bbs = thread_window->get_bbs_detail();
+      if (!bbs.belongs_to(*bbs_)) continue;
+
+      const std::string thread = bbs.get_thread_id();
+      Iter it = buffer.find(thread);
+      int res = 0;
+      if (it != buffer.end())
+        res = model_column::field<model_column::ResNum>(it->second);
+      inform_to_thread(*thread_window, res);
+    }
+  }
+}
+
+void BoardWindow::inform_to_thread(ThreadWindow& window, int res_num) const {
+  window.on_informed_from_board(res_num);
+}
+
 
 } // namespace dialektos