1 #include "tmeditorwidget.h"
12 #include <QDesktopServices>
14 #include <QVBoxLayout>
17 #include <QMutexLocker>
20 #include <QTextDocument>
21 #include <QTextCursor>
23 #include <QTextBlockFormat>
35 TM::EditorWidget::EditorWidget(Settings *settings, Service *service, QWidget *parent)
37 , m_mutex(QMutex::Recursive)
39 , m_settings(settings)
43 QVBoxLayout *vlayout = new QVBoxLayout(this);
44 vlayout->setSpacing(4);
45 vlayout->setContentsMargins(0, 0, 0, 0);
47 m_toolbar = new QToolBar("Editor", this);
49 m_edit_mode = new QAction(QIcon(":/edit.png"), "edit", this);
50 m_edit_mode->setCheckable(true);
51 m_toolbar->addAction(m_edit_mode);
52 connect(m_edit_mode, SIGNAL(triggered(bool)), this, SLOT(onEditModeTriggered(bool)));
54 m_link = new QAction(QIcon(":/link.png"), "link", this);
55 m_link->setCheckable(true);
56 m_link->setDisabled(true);
57 m_toolbar->addAction(m_link);
58 connect(m_link, SIGNAL(triggered(bool)), this, SLOT(onLinkModeTriggered(bool)));
60 m_slang = new QAction("&Source", this);
61 m_slang->setDisabled(true);
62 m_slang->setMenu(new QMenu(this));
63 m_tlang = new QAction("&Target", this);
64 m_tlang->setDisabled(true);
65 m_tlang->setMenu(new QMenu(this));
66 m_toolbar->addAction(m_slang);
67 m_toolbar->addAction(m_tlang);
69 QAction *browser = new QAction(QIcon(":/browser.png"), "browser", this);
70 m_toolbar->addAction(browser);
71 connect(browser, SIGNAL(triggered(bool)), this, SLOT(onBrowserTriggered(bool)));
73 QAction *setting = new QAction(QIcon(":/setting.png"), "setting", this);
74 m_toolbar->addAction(setting);
76 m_edit = new Editor(settings, service, this);
77 vlayout->addWidget(m_toolbar);
78 vlayout->addWidget(m_edit);
80 connect(m_service, SIGNAL(languageLoaded(int, QString, QIcon)),
81 this, SLOT(onLanguageLoaded(int,QString,QIcon)));
85 * \brief ブラウザ上でドキュメントがフォーカスを取得したとき、
86 * SocketConnectionから呼び出されます。
88 void TM::EditorWidget::attach(SocketConnection *socket)
90 if(m_socket == socket) return;
91 if(m_socket) detach(m_socket);
93 QMutexLocker lock(&m_mutex);
95 connect(this, SIGNAL(editModeChanged(bool)), m_socket, SLOT(changeEditMode(bool)));
96 set_link_mode_disabled(true);
100 * \brief ブラウザ上でドキュメントがフォーカスを取得することで、
101 * 現在のSocketConnectionがフォーカスを失った時に呼び出されます。
103 void TM::EditorWidget::detach(SocketConnection *)
105 QMutexLocker lock(&m_mutex);
106 disconnect(this, SIGNAL(editModeChanged(bool)), m_socket, SLOT(changeEditMode(bool)));
112 void TM::EditorWidget::set_http_port(quint16 http_port) { m_http_port = http_port; }
115 * \brief 編集モードを変更します。
117 void TM::EditorWidget::set_edit_mode(bool mode)
119 QMutexLocker lock(&m_mutex);
120 bool old_mode = m_edit_mode->isChecked();
121 if(old_mode == mode) return;
123 m_edit_mode->setChecked(mode);
127 * \brief 編集モードの場合trueを返します。
129 bool TM::EditorWidget::edit_mode()
131 QMutexLocker lock(&m_mutex);
132 return m_edit_mode->isChecked();
136 * \brief リンクモードを設定します。
138 void TM::EditorWidget::set_link_mode(bool mode)
140 QMutexLocker lock(&m_mutex);
141 bool old_mode = m_link->isChecked();
142 if(old_mode == mode) return;
144 m_link->setChecked(mode);
148 * \brief リンクモードの場合、trueを返します。
150 bool TM::EditorWidget::link_mode()
152 QMutexLocker lock(&m_mutex);
153 return m_link->isChecked();
157 * \brief リンクボタンをグレーアウトします。
159 void TM::EditorWidget::set_link_mode_disabled(bool disable)
161 QMutexLocker lock(&m_mutex);
162 m_link->setDisabled(disable);
166 * \brief 原文の言語コードを返します。
168 int TM::EditorWidget::source_language()
170 QMutexLocker lock(&m_mutex);
171 return m_slang->data().toInt();
175 * \brief 訳文の言語コードを返します。
177 int TM::EditorWidget::target_language()
179 QMutexLocker lock(&m_mutex);
180 return m_tlang->data().toInt();
183 void TM::EditorWidget::set_segment(TextSegment::pointer segment)
185 QMutexLocker lock(&m_mutex);
186 m_edit->set_segment(segment);
189 void TM::EditorWidget::save_sentence(int segment_id, int index)
191 m_socket->save_sentence(segment_id, index);
195 * \brief 言語プラグインが読み込まれるたびに呼び出されます。
198 * \param icon 言語を表すアイコン。
200 void TM::EditorWidget::onLanguageLoaded(int code, QString name, QIcon icon)
202 QString dslanguage = m_settings->value("Widget/defaultSourceLanguage", "English").toString();
203 QString tslanguage = m_settings->value("Widget/defaultSourceLanguage", "Japanese").toString();
204 if(dslanguage == name)
206 m_slang->setIcon(icon);
207 m_slang->setData(code);
209 if(tslanguage == name)
211 m_tlang->setIcon(icon);
212 m_tlang->setData(code);
214 QAction *saction = m_slang->menu()->addAction(icon, name);
215 saction->setData(code);
216 connect(saction, SIGNAL(triggered(bool)), this, SLOT(onSourceLanguageTriggered(bool)));
217 QAction *taction = m_tlang->menu()->addAction(icon, name);
218 taction->setData(code);
219 connect(taction, SIGNAL(triggered(bool)), this, SLOT(onTargetLanguageTriggered(bool)));
223 * \brief 編集アイコンがクリックされたとき呼び出されます。
225 void TM::EditorWidget::onEditModeTriggered(bool)
227 QAction *edit_mode = qobject_cast<QAction*>(sender());
228 bool checked = edit_mode->isChecked();
229 m_link->setDisabled(true);
230 m_slang->setDisabled(!checked);
231 m_tlang->setDisabled(!checked);
232 emit editModeChanged(checked);
234 m_edit->set_edit_mode(checked);
238 * \brief リンクアイコンがクリックされたとき呼び出されます。
240 void TM::EditorWidget::onLinkModeTriggered(bool checked)
242 //QAction *link_mode = qobject_cast<QAction*>(sender());
243 m_edit->set_link_mode(checked);
247 * \brief アクションから原文言語が変更されたとき呼び出されます。
249 void TM::EditorWidget::onSourceLanguageTriggered(bool)
251 QAction *saction = qobject_cast<QAction*>(sender());
253 m_slang->setIcon(saction->icon());
254 m_slang->setData(saction->data());
258 * \brief アクションから訳文言語が変更されたとき呼び出されます。
260 void TM::EditorWidget::onTargetLanguageTriggered(bool)
262 QAction *taction = qobject_cast<QAction*>(sender());
264 m_tlang->setIcon(taction->icon());
265 m_tlang->setData(taction->data());
269 * \brief ブラウザ・アイコンがクリックされたとき呼び出されます。
271 void TM::EditorWidget::onBrowserTriggered(bool)
273 QString url("http://localhost:");
274 url += QString::number(m_http_port) + "/";
276 QDesktopServices::openUrl(QUrl(url));
279 // EditorPanel ----------------------------------------------------------------
281 TM::EditorPanel::EditorPanel(QWidget *parent)
283 , m_parent_editor(nullptr)
287 TM::Editor* TM::EditorPanel::parent_editor() { return m_parent_editor; }
290 * \brief 親となるエディタを設定します。
292 void TM::EditorPanel::set_parent_editor(Editor *editor)
294 assert(!m_parent_editor);
295 m_parent_editor = editor;
301 void TM::EditorPanel::ensure_sentence()
303 if(!sentence()) return;
305 QTextCursor c = textCursor();
306 for(Text::pointer p = sentence()->begin(); p; p = p->next()) // p=word
309 QVariant v = QVariant::fromValue(Text::weak_pointer(p));
310 cf.setProperty(Word, v);
311 c.insertText(p->string(), cf);
316 * \brief 内容が空の場合、trueを返します。
318 bool TM::EditorPanel::is_empty() const { return document()->isEmpty(); }
321 * \brief 引数として与えられた単語に該当する範囲を選択するカーソルを返します。
323 QTextCursor TM::EditorPanel::select_cursor(Text::pointer word)
325 RangeData *rd = static_cast<RangeData*>(word->data().get());
327 int slength = word->string().length();
328 QTextCursor c = textCursor();
329 c.movePosition(QTextCursor::Start);
330 c.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, rd->begin());
331 c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, slength);
336 * \brief 引数として与えられた位置にある単語を返します。
338 Text::pointer TM::EditorPanel::select_word(QPoint const &pos)
340 QTextCursor c = cursorForPosition(pos);
342 QVariant v = c.charFormat().property(Word);
343 if(!v.isValid()) return Text::pointer();
344 Text::weak_pointer wp = v.value<Text::weak_pointer>();
345 assert(!wp.expired());
346 Text::pointer p = wp.lock();
351 * \brief wordの範囲に背景色colorを設定します。
353 * 透過色は、Qt::transparentです。
355 void TM::EditorPanel::highlight(Text::pointer word, QColor color)
357 QTextCursor c = select_cursor(word);
358 QTextCharFormat cf = c.charFormat();
359 cf.setBackground(color);
360 c.mergeCharFormat(cf);
363 void TM::EditorPanel::highlight(WordLink::storage_type *link, QColor color)
365 for(Text::pointer p : *link) highlight(p, color);
368 void TM::EditorPanel::clear_highlight() { ensure_sentence(); }
371 * \brief 引数として与えられた整数値から色を作成します。
373 * 色分け用に使えるQColorを返します。
375 QColor TM::EditorPanel::color(int index) const
378 result.setHsv(index * 199, 128, 128, 128);
382 // SourcePanel ----------------------------------------------------------------
384 TM::SourcePanel::SourcePanel(QWidget *parent)
385 : EditorPanel(parent)
387 , m_target_panel(nullptr)
389 setUndoRedoEnabled(false);
390 setContextMenuPolicy(Qt::NoContextMenu);
393 int TM::SourcePanel::index() const { return m_index; }
395 void TM::SourcePanel::set_index(int index) { m_index = index; }
397 Text::pointer TM::SourcePanel::sentence() { return source_sentence(); }
399 Text::pointer TM::SourcePanel::source_sentence()
401 return m_text_sentence->source_sentence();
404 Text::pointer TM::SourcePanel::target_sentence()
406 return m_text_sentence->target_sentence();
409 void TM::SourcePanel::set_target_sentence(Text::pointer sentence)
411 m_text_sentence->set_target_sentence(sentence);
414 TM::TextSentence::pointer TM::SourcePanel::text_sentence()
416 return m_text_sentence;
419 void TM::SourcePanel::set_text_sentence(TextSentence::pointer text_sentence)
421 m_text_sentence = text_sentence;
423 assert(target_panel());
424 target_panel()->ensure_sentence();
427 TM::TargetPanel* TM::SourcePanel::target_panel() { return m_target_panel; }
429 void TM::SourcePanel::set_target_panel(TargetPanel *target)
431 m_target_panel = target;
434 void TM::SourcePanel::commit_link()
438 m_target_panel->clear_highlight();
441 TM::WordLinker* TM::SourcePanel::linker()
443 return m_text_sentence->linker();
447 * \brief 保持するリンクを色分け表示します。
449 void TM::SourcePanel::ensure_highlight()
452 m_target_panel->clear_highlight();
454 WordLink::pointer wl = linker()->current();
457 highlight(wl->sources(), Qt::cyan);
458 target_panel()->highlight(wl->targets(), Qt::cyan);
461 for(WordLink::pointer wl : *linker())
463 int index = linker()->index_of(wl);
464 QColor c = color(index);
465 highlight(wl->sources(), c);
466 target_panel()->highlight(wl->targets(), c);
470 bool TM::SourcePanel::canInsertFromMimeData(const QMimeData *) const
472 return false; // ドロップ禁止カーソルにする
475 void TM::SourcePanel::insertFromMimeData(const QMimeData *) { } // ドロップされても何もしない
477 void TM::SourcePanel::inputMethodEvent(QInputMethodEvent *ev)
479 ev->setCommitString(""); // 入力禁止
480 QPlainTextEdit::inputMethodEvent(ev);
483 void TM::SourcePanel::keyPressEvent(QKeyEvent *ev)
485 Editor *editor = parent_editor();
491 if(editor->link_mode()) editor->set_link_mode(false);
494 if(!ev->modifiers().testFlag(Qt::ControlModifier)) break;
499 TextPanel::keyPressEvent(ev);
504 void TM::SourcePanel::do_click(QPoint const &pos)
506 Editor *editor = parent_editor();
507 if(editor->link_mode()) do_click_in_link_mode(pos);
510 void TM::SourcePanel::do_click_in_link_mode(QPoint const &pos)
512 Text::pointer w = select_word(pos);
515 linker()->toggle(WordLink::Source, w);
517 setTextCursor(cursorForPosition(pos));
520 // TargetPanel ----------------------------------------------------------------
522 TM::TargetPanel::TargetPanel(QWidget *parent)
523 : EditorPanel(parent)
524 , m_source_panel(nullptr)
525 , m_text_dirty(false)
528 //setContextMenuPolicy(Qt::NoContextMenu);
531 TM::SourcePanel* TM::TargetPanel::source_panel() { return m_source_panel; }
533 void TM::TargetPanel::set_source_panel(SourcePanel *source)
535 m_source_panel = source;
538 Text::pointer TM::TargetPanel::sentence()
540 return source_panel()->target_sentence();
543 void TM::TargetPanel::save_sentence()
548 void TM::TargetPanel::ensure_sentence()
550 EditorPanel::ensure_sentence();
551 set_text_dirty(false);
552 set_text_saved(false);
556 * \brief データベースへの登録が必要な場合、falseを返します。
558 * パネル内の文字列が編集された場合、trueを返します。
559 * 原文パネルがフォーカスを失うと、エディタがこのメンバを呼び出し、
560 * データベース登録の必要性を判定します。
561 * その後、エディタはデータベース登録を行い、フラグをクリアするので、
562 * 次に文字列が編集されるまでtrueを返し続けます。
564 bool TM::TargetPanel::is_text_saved() const { return m_text_saved; }
566 void TM::TargetPanel::set_text_saved(bool saved)
568 m_text_saved = saved;
572 * \brief パネル内の文字列が編集された場合、trueを返します。
574 * エディタがリンクモードに入るとき、このメンバを呼び出し、
575 * 単語へ分割する必要があるか判定します。
576 * エディタはパネルの文字列を単語に分割した後、set_sentence()によって
577 * パネルに文を設定するため、次に文字列を編集するまでfalseを返すようになります。
579 bool TM::TargetPanel::is_text_dirty() const { return m_text_dirty; }
582 * \brief パネルの文字列が編集されたかを示すフラグを設定します。
584 void TM::TargetPanel::set_text_dirty(bool dirty)
586 EditorWidget *editor_widget = parent_editor()->parent_editor_widget();
587 m_text_dirty = dirty;
588 if(dirty) editor_widget->set_link_mode_disabled(false);
591 bool TM::TargetPanel::canInsertFromMimeData(QMimeData const *source) const
593 if(source->hasText()) return true;
597 void TM::TargetPanel::insertFromMimeData(QMimeData const *source)
599 TextPanel::insertFromMimeData(source);
600 set_text_dirty(true);
601 set_text_saved(false);
604 void TM::TargetPanel::inputMethodEvent(QInputMethodEvent *ev)
606 Editor *editor = parent_editor();
607 if(editor->link_mode()) ev->setCommitString("");
608 if(!ev->commitString().isEmpty())
610 set_text_dirty(true);
611 set_text_saved(false);
613 QPlainTextEdit::inputMethodEvent(ev);
616 void TM::TargetPanel::keyPressEvent(QKeyEvent *ev)
618 Editor *editor = parent_editor();
619 if(editor->link_mode()) do_key_press_in_link_mode(ev);
622 TextPanel::keyPressEvent(ev);
623 if(!ev->text().isEmpty())
625 set_text_dirty(true);
626 set_text_saved(false);
631 void TM::TargetPanel::do_click(QPoint const &pos)
633 Editor *editor = parent_editor();
634 if(editor->link_mode()) do_click_in_link_mode(pos);
637 void TM::TargetPanel::do_click_in_link_mode(QPoint const &pos)
639 Text::pointer w = select_word(pos);
642 WordLinker *wl = m_source_panel->linker();
643 wl->toggle(WordLink::Target, w);
644 m_source_panel->ensure_highlight();
645 setTextCursor(cursorForPosition(pos));
648 void TM::TargetPanel::do_key_press_in_link_mode(QKeyEvent *ev)
650 Editor *editor = parent_editor();
656 editor->set_link_mode(false);
662 TextPanel::keyPressEvent(ev);
667 // Editor ---------------------------------------------------------------------
669 TM::Editor::Editor(Settings *settings, Service *service, QWidget *parent)
671 , m_settings(settings)
676 , m_current_source_panel(nullptr)
678 TextArea *ta = text_area();
679 connect(ta, SIGNAL(focusInChild(TextPanel*,TextPanel*)), this, SLOT(onFocusInChild(TextPanel*,TextPanel*)));
683 * \brief エディタが保持しているリソースを適切に解放します。
685 void TM::Editor::clear()
687 if(m_segment_id < 0) return; // 文を保持していない場合、何もしない。
690 if(m_current_source_panel) do_panel_leave(m_current_source_panel);
691 m_current_source_panel = nullptr;
693 TextArea *ta = text_area();
696 set_link_mode(false);
697 parent_editor_widget()->set_link_mode_disabled(true);
702 void TM::Editor::set_segment(TextSegment::pointer segment)
705 m_segment_id = segment->segment_id();
706 TextArea *ta = text_area();
708 for(TextSentence::pointer p : *segment)
710 SourcePanel *sp = ta->append<SourcePanel>();
711 TargetPanel *tp = ta->append<TargetPanel>();
713 sp->set_target_panel(tp);
714 sp->set_parent_editor(this);
715 tp->set_source_panel(sp);
716 tp->set_parent_editor(this);
718 sp->set_text_sentence(p);
724 bool TM::Editor::edit_mode() const { return m_edit_mode; }
726 void TM::Editor::set_edit_mode(bool mode_) { m_edit_mode = mode_; }
728 bool TM::Editor::link_mode() const { return m_link_mode; }
730 void TM::Editor::set_link_mode(bool mode_)
732 if(mode_ == m_link_mode) return; // モードが変化していない場合、何もしない。
733 assert(!mode_ || can_link_mode()); // リンクモードに入るには、条件がある。
736 parent_editor_widget()->set_link_mode(mode_);
738 if(mode_) do_link_mode_enter(m_current_source_panel);
739 else if(m_current_source_panel) do_link_mode_leave(m_current_source_panel);
742 bool TM::Editor::can_link_mode() const
744 if(!m_current_source_panel) return false;
745 if(!edit_mode()) return false;
746 if(current_target_panel()->is_empty()) return false;
751 TM::EditorWidget* TM::Editor::parent_editor_widget()
753 EditorWidget* result = qobject_cast<EditorWidget*>(parentWidget());
758 TM::TargetPanel* TM::Editor::current_target_panel()
760 TargetPanel *result = nullptr;
761 if(m_current_source_panel) result = m_current_source_panel->target_panel();
765 TM::TargetPanel const* TM::Editor::current_target_panel() const
767 return const_cast<Editor*>(this)->current_target_panel();
770 void TM::Editor::onFocusInChild(TextPanel *new_, TextPanel *)
772 SourcePanel *old_panel = m_current_source_panel;
773 SourcePanel *new_panel = find_source_panel(new_);
776 if(old_panel && old_panel != new_panel) do_panel_leave(old_panel);
777 m_current_source_panel = new_panel;
778 if(old_panel != new_panel) do_panel_enter(new_panel);
781 TM::SourcePanel* TM::Editor::find_source_panel(TextPanel *panel)
783 SourcePanel *sp = qobject_cast<SourcePanel*>(panel);
785 TargetPanel *tp = qobject_cast<TargetPanel*>(panel);
786 if(tp) return tp->source_panel();
791 * \brief 文を表示するパネルにフォーカスが入るとき呼び出されます。
793 void TM::Editor::do_panel_enter(SourcePanel *panel)
795 TargetPanel *tp = panel->target_panel();
798 if(can_link_mode()) parent_editor_widget()->set_link_mode_disabled(false);
802 * \brief 文を表示するパネルからフォーカスが出るとき呼び出されます。
804 void TM::Editor::do_panel_leave(SourcePanel *panel)
806 TargetPanel *tp = panel->target_panel();
810 set_link_mode(false);
811 parent_editor_widget()->set_link_mode_disabled(true);
813 if(!tp->is_text_saved())
815 if(tp->is_text_dirty()) divide_target_sentence(panel);
816 int index = panel->index();
817 parent_editor_widget()->save_sentence(m_segment_id, index);
818 tp->set_text_saved(true);
823 * \brief リンクモードに入るとき呼び出されます。
825 void TM::Editor::do_link_mode_enter(SourcePanel *panel)
828 TargetPanel *tp = panel->target_panel();
831 if(tp->is_text_dirty()) divide_target_sentence(panel);
832 panel->ensure_highlight();
836 * \brief リンクモードから出るとき呼び出されます。
838 void TM::Editor::do_link_mode_leave(SourcePanel *panel)
841 panel->commit_link();
842 //panel->clear_highlight();
846 * \brief 訳文パネルの文を単語に分割し、表示に反映します。
848 void TM::Editor::divide_target_sentence(SourcePanel *source_panel)
850 int tcode = parent_editor_widget()->target_language();
851 TargetPanel *tp = source_panel->target_panel();
852 QString string = tp->toPlainText();
854 Text::pointer sentences = m_service->divide_into_sentences(tcode, string);
855 if(sentences->size())
857 source_panel->linker()->clear();
858 Text::pointer words = m_service->divide_into_words(tcode, sentences->begin());
859 source_panel->set_target_sentence(words);
860 tp->ensure_sentence(); // ココで、text_dirtyがfalseになる。