1 #include "tmeditorwidget.h"
12 #include <QDesktopServices>
14 #include <QVBoxLayout>
17 #include <QMutexLocker>
20 #include <QTextDocument>
21 #include <QTextCursor>
23 #include <QTextBlockFormat>
36 TM::EditorWidget::EditorWidget(Settings *settings, Service *service, QWidget *parent)
38 , m_mutex(QMutex::Recursive)
40 , m_settings(settings)
44 QVBoxLayout *vlayout = new QVBoxLayout(this);
45 vlayout->setSpacing(4);
46 vlayout->setContentsMargins(0, 0, 0, 0);
48 m_toolbar = new QToolBar("editor", this);
50 m_edit_mode = new QAction(QIcon(":/edit.png"), "edit", this);
51 m_edit_mode->setCheckable(true);
52 m_toolbar->addAction(m_edit_mode);
53 connect(m_edit_mode, SIGNAL(triggered(bool)), this, SLOT(onEditModeTriggered(bool)));
55 m_link = new QAction(QIcon(":/link.png"), "link", this);
56 m_link->setCheckable(true);
57 m_link->setDisabled(true);
58 m_toolbar->addAction(m_link);
59 connect(m_link, SIGNAL(triggered(bool)), this, SLOT(onLinkModeTriggered(bool)));
61 m_slang = new QAction("&source", this);
62 //m_slang->setDisabled(true);
63 m_slang->setMenu(new QMenu(this));
64 m_tlang = new QAction("&target", this);
65 //m_tlang->setDisabled(true);
66 m_tlang->setMenu(new QMenu(this));
67 m_toolbar->addAction(m_slang);
68 m_toolbar->addAction(m_tlang);
70 QAction *browser = new QAction(QIcon(":/browser.png"), "browser", this);
71 m_toolbar->addAction(browser);
72 connect(browser, SIGNAL(triggered(bool)), this, SLOT(onBrowserTriggered(bool)));
74 QAction *setting = new QAction(QIcon(":/setting.png"), "setting", this);
75 m_toolbar->addAction(setting);
77 m_edit = new Editor(settings, service, this);
78 vlayout->addWidget(m_toolbar);
79 vlayout->addWidget(m_edit);
81 connect(m_service, SIGNAL(languageLoaded(int, QString, QIcon)),
82 this, SLOT(onLanguageLoaded(int,QString,QIcon)));
86 * \brief ブラウザ上でドキュメントがフォーカスを取得したとき、
87 * SocketConnectionから呼び出されます。
89 void TM::EditorWidget::attach(SocketConnection *socket)
91 if(m_socket == socket) return;
92 if(m_socket) detach(m_socket);
94 QMutexLocker lock(&m_mutex);
96 connect(this, SIGNAL(editModeChanged(bool)), m_socket, SLOT(changeEditMode(bool)));
97 set_link_mode_disabled(true);
98 connect(this, SIGNAL(sourceLanguageChanged()), m_socket, SLOT(changeLanguage()));
99 connect(this, SIGNAL(targetLanguageChanged()), m_socket, SLOT(changeLanguage()));
103 * \brief ブラウザ上でドキュメントがフォーカスを取得することで、
104 * 現在のSocketConnectionがフォーカスを失った時に呼び出されます。
106 void TM::EditorWidget::detach(SocketConnection *)
108 QMutexLocker lock(&m_mutex);
109 disconnect(this, SIGNAL(editModeChanged(bool)), m_socket, SLOT(changeEditMode(bool)));
110 disconnect(this, SIGNAL(sourceLanguageChanged()), m_socket, SLOT(changeLanguage()));
111 disconnect(this, SIGNAL(targetLanguageChanged()), m_socket, SLOT(changeLanguage()));
117 void TM::EditorWidget::set_http_port(quint16 http_port) { m_http_port = http_port; }
120 * \brief 編集モードを変更します。
122 void TM::EditorWidget::set_edit_mode(bool mode)
124 QMutexLocker lock(&m_mutex);
125 bool old_mode = m_edit_mode->isChecked();
126 if(old_mode == mode) return;
128 m_edit_mode->setChecked(mode);
132 * \brief 編集モードの場合trueを返します。
134 bool TM::EditorWidget::edit_mode()
136 QMutexLocker lock(&m_mutex);
137 return m_edit_mode->isChecked();
141 * \brief リンクモードを設定します。
143 void TM::EditorWidget::set_link_mode(bool mode)
145 QMutexLocker lock(&m_mutex);
146 bool old_mode = m_link->isChecked();
147 if(old_mode == mode) return;
149 m_link->setChecked(mode);
153 * \brief リンクモードの場合、trueを返します。
155 bool TM::EditorWidget::link_mode()
157 QMutexLocker lock(&m_mutex);
158 return m_link->isChecked();
162 * \brief リンクボタンをグレーアウトします。
164 void TM::EditorWidget::set_link_mode_disabled(bool disable)
166 QMutexLocker lock(&m_mutex);
167 m_link->setDisabled(disable);
171 * \brief 原文の言語コードを返します。
173 int TM::EditorWidget::source_language()
175 QMutexLocker lock(&m_mutex);
176 return m_slang->data().toInt();
180 * \brief 訳文の言語コードを返します。
182 int TM::EditorWidget::target_language()
184 QMutexLocker lock(&m_mutex);
185 return m_tlang->data().toInt();
188 void TM::EditorWidget::set_segment(TextSegment::pointer segment)
190 QMutexLocker lock(&m_mutex);
191 m_edit->set_segment(segment);
195 * \brief SocketConnectionにセンテンスの保存を要求します。
196 * \param segment_id セグメントのID
197 * \param index センテンスのインデックス
199 * 結果としてデータベース、サーバ、ブラウザを更新します。
201 void TM::EditorWidget::save_sentence(int segment_id, int index)
203 m_socket->save_sentence(segment_id, index);
207 * \brief SocketConnectionにセンテンスのクリアを要求します。
208 * \param segment_id セグメントのID
209 * \param index センテンスのインデックス
211 * 結果としてデータベース、サーバ、ブラウザを更新します。
213 * チケット #35438 訳文を元に戻せない 対応
215 void TM::EditorWidget::remove_sentence(int segment_id, int index)
217 m_socket->remove_sentence(segment_id, index);
221 * \brief 言語プラグインが読み込まれるたびに呼び出されます。
224 * \param icon 言語を表すアイコン。
226 void TM::EditorWidget::onLanguageLoaded(int code, QString name, QIcon icon)
228 if(!m_settings->contains("Widget/default_source_language"))
229 m_settings->setValue("Widget/default_source_language", QLocale::Language::English);
230 if(!m_settings->contains("Widget/default_target_language"))
231 m_settings->setValue("Widget/default_target_language", QLocale::Language::Japanese);
233 int scode = m_settings->value("Widget/default_source_language").toInt();
234 int tcode = m_settings->value("Widget/default_target_language").toInt();
237 m_slang->setIcon(icon);
238 m_slang->setData(code);
242 m_tlang->setIcon(icon);
243 m_tlang->setData(code);
245 QAction *saction = m_slang->menu()->addAction(icon, name);
246 saction->setData(code);
247 connect(saction, SIGNAL(triggered(bool)), this, SLOT(onSourceLanguageTriggered(bool)));
248 QAction *taction = m_tlang->menu()->addAction(icon, name);
249 taction->setData(code);
250 connect(taction, SIGNAL(triggered(bool)), this, SLOT(onTargetLanguageTriggered(bool)));
254 * \brief 編集アイコンがクリックされたとき呼び出されます。
256 void TM::EditorWidget::onEditModeTriggered(bool)
258 QAction *edit_mode = qobject_cast<QAction*>(sender());
259 bool checked = edit_mode->isChecked();
260 m_link->setDisabled(true);
261 //m_slang->setDisabled(!checked);
262 //m_tlang->setDisabled(!checked);
263 emit editModeChanged(checked);
265 m_edit->set_edit_mode(checked);
269 * \brief リンクアイコンがクリックされたとき呼び出されます。
271 void TM::EditorWidget::onLinkModeTriggered(bool checked)
273 //QAction *link_mode = qobject_cast<QAction*>(sender());
274 m_edit->set_link_mode(checked);
278 * \brief アクションから原文言語が変更されたとき呼び出されます。
280 void TM::EditorWidget::onSourceLanguageTriggered(bool)
282 QAction *saction = qobject_cast<QAction*>(sender());
284 m_slang->setIcon(saction->icon());
285 m_slang->setData(saction->data());
287 m_settings->setValue("Widget/default_source_language", saction->data());
289 emit sourceLanguageChanged();
293 * \brief アクションから訳文言語が変更されたとき呼び出されます。
295 void TM::EditorWidget::onTargetLanguageTriggered(bool)
297 QAction *taction = qobject_cast<QAction*>(sender());
299 m_tlang->setIcon(taction->icon());
300 m_tlang->setData(taction->data());
302 m_settings->setValue("Widget/default_target_language", taction->data());
304 emit targetLanguageChanged();
308 * \brief ブラウザ・アイコンがクリックされたとき呼び出されます。
310 void TM::EditorWidget::onBrowserTriggered(bool)
312 QString url("http://localhost:");
313 url += QString::number(m_http_port) + "/";
315 QDesktopServices::openUrl(QUrl(url));
318 // EditorPanel ----------------------------------------------------------------
320 TM::EditorPanel::EditorPanel(QWidget *parent)
322 , m_parent_editor(nullptr)
326 TM::Editor* TM::EditorPanel::parent_editor() { return m_parent_editor; }
329 * \brief 親となるエディタを設定します。
331 void TM::EditorPanel::set_parent_editor(Editor *editor)
333 assert(!m_parent_editor);
334 m_parent_editor = editor;
340 void TM::EditorPanel::ensure_sentence()
342 if(!sentence()) return;
344 QTextCursor c = textCursor();
345 for(Text::pointer p = sentence()->begin(); p; p = p->next()) // p=word
348 QVariant v = QVariant::fromValue(Text::weak_pointer(p));
349 cf.setProperty(Word, v);
350 c.insertText(p->string(), cf);
355 * \brief 内容が空の場合、trueを返します。
357 bool TM::EditorPanel::is_empty() const { return document()->isEmpty(); }
360 * \brief 引数として与えられた単語に該当する範囲を選択するカーソルを返します。
362 QTextCursor TM::EditorPanel::select_cursor(Text::pointer word)
364 RangeData *rd = static_cast<RangeData*>(word->data().get());
366 int slength = word->string().length();
367 QTextCursor c = textCursor();
368 c.movePosition(QTextCursor::Start);
369 c.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, rd->begin());
370 c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, slength);
375 * \brief 引数として与えられた位置にある単語を返します。
377 Text::pointer TM::EditorPanel::select_word(QPoint const &pos)
379 QTextCursor c = cursorForPosition(pos);
381 QVariant v = c.charFormat().property(Word);
382 if(!v.isValid()) return Text::pointer();
383 Text::weak_pointer wp = v.value<Text::weak_pointer>();
384 assert(!wp.expired());
385 Text::pointer p = wp.lock();
390 * \brief wordの範囲に背景色colorを設定します。
392 * 透過色は、Qt::transparentです。
394 void TM::EditorPanel::highlight(Text::pointer word, QColor color)
396 QTextCursor c = select_cursor(word);
397 QTextCharFormat cf = c.charFormat();
398 cf.setBackground(color);
399 c.mergeCharFormat(cf);
402 void TM::EditorPanel::highlight(WordLink::storage_type *link, QColor color)
404 for(Text::pointer p : *link) highlight(p, color);
407 void TM::EditorPanel::clear_highlight() { ensure_sentence(); }
410 * \brief 引数として与えられた整数値から色を作成します。
412 * 色分け用に使えるQColorを返します。
414 QColor TM::EditorPanel::color(int index) const
417 result.setHsv(index * 199, 128, 128, 128);
421 // SourcePanel ----------------------------------------------------------------
423 TM::SourcePanel::SourcePanel(QWidget *parent)
424 : EditorPanel(parent)
426 , m_target_panel(nullptr)
428 setUndoRedoEnabled(false);
429 setContextMenuPolicy(Qt::NoContextMenu);
432 int TM::SourcePanel::index() const { return m_index; }
434 void TM::SourcePanel::set_index(int index) { m_index = index; }
436 Text::pointer TM::SourcePanel::sentence() { return source_sentence(); }
438 Text::pointer TM::SourcePanel::source_sentence()
440 return m_text_sentence->source_sentence();
443 Text::pointer TM::SourcePanel::target_sentence()
445 return m_text_sentence->target_sentence();
448 void TM::SourcePanel::set_target_sentence(Text::pointer sentence)
450 m_text_sentence->set_target_sentence(sentence);
453 TM::TextSentence::pointer TM::SourcePanel::text_sentence()
455 return m_text_sentence;
458 void TM::SourcePanel::set_text_sentence(TextSentence::pointer text_sentence)
460 m_text_sentence = text_sentence;
462 assert(target_panel());
463 target_panel()->ensure_sentence();
466 TM::TargetPanel* TM::SourcePanel::target_panel() { return m_target_panel; }
468 void TM::SourcePanel::set_target_panel(TargetPanel *target)
470 m_target_panel = target;
473 void TM::SourcePanel::commit_link()
477 m_target_panel->clear_highlight();
480 TM::WordLinker* TM::SourcePanel::linker()
482 return m_text_sentence->linker();
486 * \brief 保持するリンクを色分け表示します。
488 void TM::SourcePanel::ensure_highlight()
491 m_target_panel->clear_highlight();
493 WordLink::pointer wl = linker()->current();
496 highlight(wl->sources(), Qt::cyan);
497 target_panel()->highlight(wl->targets(), Qt::cyan);
500 for(WordLink::pointer wl : *linker())
502 int index = linker()->index_of(wl);
503 QColor c = color(index);
504 highlight(wl->sources(), c);
505 target_panel()->highlight(wl->targets(), c);
509 bool TM::SourcePanel::canInsertFromMimeData(const QMimeData *) const
511 return false; // ドロップ禁止カーソルにする
514 void TM::SourcePanel::insertFromMimeData(const QMimeData *) { } // ドロップされても何もしない
516 void TM::SourcePanel::inputMethodEvent(QInputMethodEvent *ev)
518 ev->setCommitString(""); // 入力禁止
519 TextPanel::inputMethodEvent(ev);
522 void TM::SourcePanel::keyPressEvent(QKeyEvent *ev)
524 Editor *editor = parent_editor();
530 if(editor->link_mode()) editor->set_link_mode(false);
533 if(!ev->modifiers().testFlag(Qt::ControlModifier)) break;
538 TextPanel::keyPressEvent(ev);
543 void TM::SourcePanel::do_click(QPoint const &pos)
545 Editor *editor = parent_editor();
546 if(editor->link_mode()) do_click_in_link_mode(pos);
549 void TM::SourcePanel::do_click_in_link_mode(QPoint const &pos)
551 Text::pointer w = select_word(pos);
554 linker()->toggle(WordLink::Source, w);
556 setTextCursor(cursorForPosition(pos));
559 // TargetPanel ----------------------------------------------------------------
561 TM::TargetPanel::TargetPanel(QWidget *parent)
562 : EditorPanel(parent)
563 , m_source_panel(nullptr)
564 , m_text_dirty(false)
567 //setContextMenuPolicy(Qt::NoContextMenu);
570 TM::SourcePanel* TM::TargetPanel::source_panel() { return m_source_panel; }
572 void TM::TargetPanel::set_source_panel(SourcePanel *source)
574 m_source_panel = source;
577 Text::pointer TM::TargetPanel::sentence()
579 return source_panel()->target_sentence();
582 void TM::TargetPanel::save_sentence()
587 void TM::TargetPanel::ensure_sentence()
589 EditorPanel::ensure_sentence();
590 set_text_dirty(false);
591 set_text_saved(false);
595 * \brief データベースへの登録が必要な場合、falseを返します。
597 * パネル内の文字列が編集された場合、trueを返します。
598 * 原文パネルがフォーカスを失うと、エディタがこのメンバを呼び出し、
599 * データベース登録の必要性を判定します。
600 * その後、エディタはデータベース登録を行い、フラグをクリアするので、
601 * 次に文字列が編集されるまでtrueを返し続けます。
603 bool TM::TargetPanel::is_text_saved() const { return m_text_saved; }
605 void TM::TargetPanel::set_text_saved(bool saved)
607 m_text_saved = saved;
611 * \brief パネル内の文字列が編集された場合、trueを返します。
613 * エディタがリンクモードに入るとき、このメンバを呼び出し、
614 * 単語へ分割する必要があるか判定します。
615 * エディタはパネルの文字列を単語に分割した後、set_sentence()によって
616 * パネルに文を設定するため、次に文字列を編集するまでfalseを返すようになります。
618 bool TM::TargetPanel::is_text_dirty() const { return m_text_dirty; }
621 * \brief パネルの文字列が編集されたかを示すフラグを設定します。
623 void TM::TargetPanel::set_text_dirty(bool dirty)
625 EditorWidget *editor_widget = parent_editor()->parent_editor_widget();
626 m_text_dirty = dirty;
627 if(dirty) editor_widget->set_link_mode_disabled(false);
630 bool TM::TargetPanel::canInsertFromMimeData(QMimeData const *source) const
632 if(source->hasText()) return true;
636 void TM::TargetPanel::insertFromMimeData(QMimeData const *source)
638 TextPanel::insertFromMimeData(source);
639 set_text_dirty(true);
640 set_text_saved(false);
643 void TM::TargetPanel::inputMethodEvent(QInputMethodEvent *ev)
645 Editor *editor = parent_editor();
646 if(editor->link_mode()) ev->setCommitString("");
647 if(!ev->commitString().isEmpty())
649 set_text_dirty(true);
650 set_text_saved(false);
652 TextPanel::inputMethodEvent(ev);
655 void TM::TargetPanel::keyPressEvent(QKeyEvent *ev)
657 Editor *editor = parent_editor();
658 if(editor->link_mode()) do_key_press_in_link_mode(ev);
661 TextPanel::keyPressEvent(ev);
662 if(!ev->text().isEmpty())
664 set_text_dirty(true);
665 set_text_saved(false);
670 void TM::TargetPanel::do_click(QPoint const &pos)
672 Editor *editor = parent_editor();
673 if(editor->link_mode()) do_click_in_link_mode(pos);
676 void TM::TargetPanel::do_click_in_link_mode(QPoint const &pos)
678 Text::pointer w = select_word(pos);
681 WordLinker *wl = m_source_panel->linker();
682 wl->toggle(WordLink::Target, w);
683 m_source_panel->ensure_highlight();
684 setTextCursor(cursorForPosition(pos));
687 void TM::TargetPanel::do_key_press_in_link_mode(QKeyEvent *ev)
689 Editor *editor = parent_editor();
695 editor->set_link_mode(false);
701 TextPanel::keyPressEvent(ev);
706 // Editor ---------------------------------------------------------------------
708 TM::Editor::Editor(Settings *settings, Service *service, QWidget *parent)
710 , m_settings(settings)
715 , m_current_source_panel(nullptr)
717 TextArea *ta = text_area();
718 connect(ta, SIGNAL(focusInChild(TextPanel*,TextPanel*)), this, SLOT(onFocusInChild(TextPanel*,TextPanel*)));
722 * \brief エディタが保持しているリソースを適切に解放します。
724 void TM::Editor::clear()
726 if(m_segment_id < 0) return; // 文を保持していない場合、何もしない。
729 if(m_current_source_panel) do_panel_leave(m_current_source_panel);
730 m_current_source_panel = nullptr;
732 TextArea *ta = text_area();
735 set_link_mode(false);
736 parent_editor_widget()->set_link_mode_disabled(true);
741 void TM::Editor::set_segment(TextSegment::pointer segment)
744 m_segment_id = segment->segment_id();
745 TextArea *ta = text_area();
747 for(TextSentence::pointer p : *segment)
749 SourcePanel *sp = ta->append<SourcePanel>();
750 TargetPanel *tp = ta->append<TargetPanel>();
752 sp->set_target_panel(tp);
753 sp->set_parent_editor(this);
754 tp->set_source_panel(sp);
755 tp->set_parent_editor(this);
757 sp->set_text_sentence(p);
763 bool TM::Editor::edit_mode() const { return m_edit_mode; }
765 void TM::Editor::set_edit_mode(bool mode_) { m_edit_mode = mode_; }
767 bool TM::Editor::link_mode() const { return m_link_mode; }
769 void TM::Editor::set_link_mode(bool mode_)
771 if(mode_ == m_link_mode) return; // モードが変化していない場合、何もしない。
772 assert(!mode_ || can_link_mode()); // リンクモードに入るには、条件がある。
775 parent_editor_widget()->set_link_mode(mode_);
777 if(mode_) do_link_mode_enter(m_current_source_panel);
778 else if(m_current_source_panel) do_link_mode_leave(m_current_source_panel);
781 bool TM::Editor::can_link_mode() const
783 if(!m_current_source_panel) return false;
784 if(!edit_mode()) return false;
785 if(current_target_panel()->is_empty()) return false;
790 TM::EditorWidget* TM::Editor::parent_editor_widget()
792 EditorWidget* result = qobject_cast<EditorWidget*>(parentWidget());
797 TM::TargetPanel* TM::Editor::current_target_panel()
799 TargetPanel *result = nullptr;
800 if(m_current_source_panel) result = m_current_source_panel->target_panel();
804 TM::TargetPanel const* TM::Editor::current_target_panel() const
806 return const_cast<Editor*>(this)->current_target_panel();
809 void TM::Editor::onFocusInChild(TextPanel *new_, TextPanel *)
811 SourcePanel *old_panel = m_current_source_panel;
812 SourcePanel *new_panel = find_source_panel(new_);
815 if(old_panel && old_panel != new_panel) do_panel_leave(old_panel);
816 m_current_source_panel = new_panel;
817 if(old_panel != new_panel) do_panel_enter(new_panel);
820 TM::SourcePanel* TM::Editor::find_source_panel(TextPanel *panel)
822 SourcePanel *sp = qobject_cast<SourcePanel*>(panel);
824 TargetPanel *tp = qobject_cast<TargetPanel*>(panel);
825 if(tp) return tp->source_panel();
830 * \brief 文を表示するパネルにフォーカスが入るとき呼び出されます。
832 void TM::Editor::do_panel_enter(SourcePanel *panel)
834 TargetPanel *tp = panel->target_panel();
837 if(can_link_mode()) parent_editor_widget()->set_link_mode_disabled(false);
841 * \brief 文を表示するパネルからフォーカスが出るとき呼び出されます。
843 void TM::Editor::do_panel_leave(SourcePanel *panel)
845 TargetPanel *tp = panel->target_panel();
849 set_link_mode(false);
850 parent_editor_widget()->set_link_mode_disabled(true);
852 int index = panel->index(); // センテンスのインデックス。
853 // チケット #35438 訳文を元に戻せない
854 bool need_clear_target = (!tp->is_text_saved()) && tp->is_text_dirty() && tp->is_empty();
855 if(need_clear_target)
857 parent_editor_widget()->remove_sentence(m_segment_id, index);
859 else if(!tp->is_text_saved())
861 if(tp->is_text_dirty()) divide_target_sentence(panel);
863 parent_editor_widget()->save_sentence(m_segment_id, index);
864 tp->set_text_saved(true);
869 * \brief リンクモードに入るとき呼び出されます。
871 void TM::Editor::do_link_mode_enter(SourcePanel *panel)
874 TargetPanel *tp = panel->target_panel();
877 if(tp->is_text_dirty()) divide_target_sentence(panel);
878 panel->ensure_highlight();
882 * \brief リンクモードから出るとき呼び出されます。
884 void TM::Editor::do_link_mode_leave(SourcePanel *panel)
887 panel->commit_link();
888 //panel->clear_highlight();
892 * \brief 訳文パネルの文を単語に分割し、表示に反映します。
894 void TM::Editor::divide_target_sentence(SourcePanel *source_panel)
896 int tcode = parent_editor_widget()->target_language();
897 TargetPanel *tp = source_panel->target_panel();
898 QString string = tp->toPlainText();
900 Text::pointer sentences = m_service->divide_into_sentences(tcode, string);
901 if(sentences->size())
903 source_panel->linker()->clear();
904 Text::pointer words = m_service->divide_into_words(tcode, sentences->begin());
905 source_panel->set_target_sentence(words);
906 tp->ensure_sentence(); // ココで、text_dirtyがfalseになる。