/* * This file is part of NeighborNote * Copyright 2013 Yuki Takahashi * * This file may be licensed under the terms of of the * GNU General Public License Version 2 (the ``GPL''). * * Software distributed under the License is distributed * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either * express or implied. See the GPL for the specific language * governing rights and limitations. * * You should have received a copy of the GPL along with this * program. If not, go to http://www.gnu.org/licenses/gpl.html * or write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ package cx.fbn.nevernote.gui; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import com.evernote.edam.type.Note; import com.trolltech.qt.QThread; import com.trolltech.qt.core.QByteArray; import com.trolltech.qt.core.QFile; import com.trolltech.qt.core.QSize; import com.trolltech.qt.core.Qt.MouseButton; import com.trolltech.qt.gui.QAction; import com.trolltech.qt.gui.QApplication; import com.trolltech.qt.gui.QContextMenuEvent; import com.trolltech.qt.gui.QListWidget; import com.trolltech.qt.gui.QListWidgetItem; import com.trolltech.qt.gui.QMenu; import cx.fbn.nevernote.Global; import cx.fbn.nevernote.NeverNote; import cx.fbn.nevernote.sql.DatabaseConnection; import cx.fbn.nevernote.threads.CounterRunner; import cx.fbn.nevernote.threads.ENRelatedNotesRunner; import cx.fbn.nevernote.threads.ENThumbnailRunner; import cx.fbn.nevernote.threads.SyncRunner; import cx.fbn.nevernote.utilities.ApplicationLogger; import cx.fbn.nevernote.utilities.Pair; public class RensoNoteList extends QListWidget { private final DatabaseConnection conn; private final ApplicationLogger logger; private final HashMap<QListWidgetItem, String> rensoNoteListItems; private final HashMap<String, RensoNoteListItem> rensoNoteListTrueItems; private String rensoNotePressedItemGuid; private final QAction openNewTabAction; private final QAction starAction; private final QAction unstarAction; private final QAction excludeNoteAction; private final NeverNote parent; private final QMenu menu; private HashMap<String, Integer> mergedHistory; // マージされた操作履歴 private final SyncRunner syncRunner; private final ENRelatedNotesRunner enRelatedNotesRunner; private final QThread enRelatedNotesThread; private final HashMap<String, List<String>> enRelatedNotesCache; // Evernote関連ノートのキャッシュ<guid, 関連ノートリスト> private final ENThumbnailRunner enThumbnailRunner; private final QThread enThumbnailThread; private String guid; public RensoNoteList(DatabaseConnection c, NeverNote p, SyncRunner syncRunner, ApplicationLogger logger) { this.logger = logger; this.logger.log(this.logger.HIGH, "Setting up rensoNoteList"); this.conn = c; this.parent = p; this.syncRunner = syncRunner; this.guid = new String(); mergedHistory = new HashMap<String, Integer>(); enRelatedNotesCache = new HashMap<String, List<String>>(); this.enRelatedNotesRunner = new ENRelatedNotesRunner(this.syncRunner, "enRelatedNotesRunner.log"); this.enRelatedNotesRunner.enRelatedNotesSignal.getENRelatedNotesFinished.connect(this, "enRelatedNotesComplete()"); this.enRelatedNotesRunner.limitSignal.rateLimitReached.connect(parent, "informRateLimit(Integer)"); this.enRelatedNotesThread = new QThread(enRelatedNotesRunner, "ENRelatedNotes Thread"); this.getEnRelatedNotesThread().start(); this.enThumbnailRunner = new ENThumbnailRunner("enThumbnailRunner.log", CounterRunner.NOTEBOOK, Global.getDatabaseUrl(), Global.getIndexDatabaseUrl(), Global.getResourceDatabaseUrl(), Global.getBehaviorDatabaseUrl(), Global.getDatabaseUserid(), Global.getDatabaseUserPassword(), Global.cipherPassword); this.enThumbnailRunner.enThumbnailSignal.getENThumbnailFinished.connect(this, "enThumbnailComplete(String)"); this.enThumbnailRunner.limitSignal.rateLimitReached.connect(parent, "informRateLimit(Integer)"); this.enThumbnailThread = new QThread(enThumbnailRunner, "ENThumbnail Thread"); this.enThumbnailThread.start(); rensoNoteListItems = new HashMap<QListWidgetItem, String>(); rensoNoteListTrueItems = new HashMap<String, RensoNoteListItem>(); this.itemPressed.connect(this, "rensoNoteItemPressed(QListWidgetItem)"); // コンテキストメニュー作成 menu = new QMenu(this); // 新しいタブで開くアクション生成 openNewTabAction = new QAction(tr("Open in New Tab"), this); openNewTabAction.setToolTip(tr("Open this note in new tab")); openNewTabAction.triggered.connect(parent, "openNewTabFromRNL()"); // スターをつけるアクション生成 starAction = new QAction(tr("Add Star"), this); starAction.setToolTip(tr("Add Star to this item")); starAction.triggered.connect(parent, "starNote()"); // スターを外すアクション生成 unstarAction = new QAction(tr("Remove Star"), this); unstarAction.setToolTip(tr("Remove Star from this item")); unstarAction.triggered.connect(parent, "unstarNote()"); // このノートを除外するアクション生成 excludeNoteAction = new QAction(tr("Exclude"), this); excludeNoteAction.setToolTip(tr("Exclude this note from RensoNoteList")); excludeNoteAction.triggered.connect(parent, "excludeNote()"); // コンテキストメニューに登録 menu.addAction(openNewTabAction); menu.addAction(excludeNoteAction); menu.aboutToHide.connect(this, "contextMenuHidden()"); this.logger.log(this.logger.HIGH, "rensoNoteList setup complete"); } // オーバーロード // 現在開いているノートの連想ノートリストをリフレッシュ public void refreshRensoNoteList() { refreshRensoNoteList(guid); } // 連想ノートリストをリフレッシュ public void refreshRensoNoteList(String guid) { logger.log(logger.HIGH, "Entering RensoNoteList.refreshRensoNoteList guid = " + guid); this.clear(); rensoNoteListItems.clear(); rensoNoteListTrueItems.clear(); mergedHistory = new HashMap<String, Integer>(); if (!this.isEnabled()) { return; } if (guid == null || guid.equals("")) { return; } this.guid = guid; // すでにEvernote関連ノートがキャッシュされているか確認 boolean isCached; isCached = enRelatedNotesCache.containsKey(guid); if (!isCached) { // キャッシュ無し // Evernoteの関連ノートを別スレッドで取得させる enRelatedNotesRunner.addGuid(guid); } else { // キャッシュ有り List<String> relatedNoteGuids = enRelatedNotesCache.get(guid); addENRelatedNotes(relatedNoteGuids); } calculateHistory(guid); repaintRensoNoteList(false); logger.log(logger.HIGH, "Leaving RensoNoteList.refreshRensoNoteList"); } // 操作履歴をデータベースから取得してノートごとの関連度を算出、その後mergedHistoryに追加 private void calculateHistory(String guid) { logger.log(logger.EXTREME, "Entering RensoNoteList.calculateHistory guid = " + guid); // browseHistory<guid, 回数(ポイント)> HashMap<String, Integer> browseHistory = conn.getHistoryTable().getBehaviorHistory("browse", guid); addWeight(browseHistory, Global.getBrowseWeight()); mergedHistory = mergeHistory(filterHistory(browseHistory), mergedHistory); // copy&pasteHistory<guid, 回数(ポイント)> HashMap<String, Integer> copyAndPasteHistory = conn.getHistoryTable().getBehaviorHistory("copy & paste", guid); addWeight(copyAndPasteHistory, Global.getCopyPasteWeight()); mergedHistory = mergeHistory(filterHistory(copyAndPasteHistory), mergedHistory); // addNewNoteHistory<guid, 回数(ポイント)> HashMap<String, Integer> addNewNoteHistory = conn.getHistoryTable().getBehaviorHistory("addNewNote", guid); addWeight(addNewNoteHistory, Global.getAddNewNoteWeight()); mergedHistory = mergeHistory(filterHistory(addNewNoteHistory), mergedHistory); // rensoItemClickHistory<guid, 回数(ポイント)> HashMap<String, Integer> rensoItemClickHistory = conn.getHistoryTable().getBehaviorHistory("rensoItemClick", guid); addWeight(rensoItemClickHistory, Global.getRensoItemClickWeight()); mergedHistory = mergeHistory(filterHistory(rensoItemClickHistory), mergedHistory); // sameTagHistory<guid, 回数(ポイント)> HashMap<String, Integer> sameTagHistory = conn.getHistoryTable().getBehaviorHistory("sameTag", guid); addWeight(sameTagHistory, Global.getSameTagWeight()); mergedHistory = mergeHistory(filterHistory(sameTagHistory), mergedHistory); // sameNotebookNoteHistory<guid, 回数(ポイント)> HashMap<String, Integer> sameNotebookHistory = conn.getHistoryTable().getBehaviorHistory("sameNotebook", guid); addWeight(sameNotebookHistory, Global.getSameNotebookWeight()); mergedHistory = mergeHistory(filterHistory(sameNotebookHistory), mergedHistory); logger.log(logger.EXTREME, "Leaving RensoNoteList.calculateHistory"); } // 操作回数に重み付けする private void addWeight(HashMap<String, Integer> history, int weight){ logger.log(logger.EXTREME, "Entering RensoNoteList.addWeight"); Set<String> keySet = history.keySet(); Iterator<String> hist_iterator = keySet.iterator(); while(hist_iterator.hasNext()){ String key = hist_iterator.next(); history.put(key, history.get(key) * weight); } logger.log(logger.EXTREME, "Leaving RensoNoteList.addWeight"); } // 連想ノートリストを再描画 private void repaintRensoNoteList(boolean needClear) { logger.log(logger.EXTREME, "Entering RensoNoteList.repaintRensoNoteList"); if (needClear) { this.clear(); rensoNoteListItems.clear(); rensoNoteListTrueItems.clear(); } if (!this.isEnabled()) { return; } addRensoNoteList(mergedHistory); logger.log(logger.EXTREME, "Leaving RensoNoteList.repaintRensoNoteList"); } // 引数1と引数2をマージしたハッシュマップを返す private HashMap<String, Integer> mergeHistory(HashMap<String, Integer> History1, HashMap<String, Integer> History2){ logger.log(logger.EXTREME, "Entering RensoNoteList.mergeHistory"); HashMap<String, Integer> mergedHistory = new HashMap<String, Integer>(); mergedHistory.putAll(History1); Set<String> keySet = History2.keySet(); Iterator<String> hist2_iterator = keySet.iterator(); while(hist2_iterator.hasNext()){ String key = hist2_iterator.next(); if(mergedHistory.containsKey(key)){ mergedHistory.put(key, mergedHistory.get(key) + History2.get(key)); }else { mergedHistory.put(key, History2.get(key)); } } logger.log(logger.EXTREME, "Leaving RensoNoteList.mergeHistory"); return mergedHistory; } // 連想ノートリストにハッシュマップのデータを追加 private void addRensoNoteList(HashMap<String, Integer> History){ logger.log(logger.EXTREME, "Entering RensoNoteList.addRensoNoteList"); enThumbnailRunner.setUser(Global.getUserInformation()); enThumbnailRunner.setServerUrl(Global.getServer()); String currentNoteGuid = new String(parent.getCurrentNoteGuid()); // 除外されているノートを連想ノート候補から除去する Iterator<String> historyIterator = History.keySet().iterator(); while (historyIterator.hasNext()) { if (conn.getExcludedTable().existNote(guid, historyIterator.next())) { historyIterator.remove(); } } // すべての関連ポイントの合計を取得(関連度のパーセント算出に利用) int allPointSum = 0; for (int p : History.values()) { allPointSum += p; } // スター付きノートとスター無しノートを分ける HashMap<String, Integer> staredNotes = new HashMap<String, Integer>(); // スター付きノートのマップ HashMap<String, Integer> normalNotes = new HashMap<String, Integer>(); // スター無しノートのマップ for (String nextGuid: History.keySet()) { int relationPoint = History.get(nextGuid); boolean isStared = conn.getStaredTable().existNote(currentNoteGuid, nextGuid); if (isStared) { staredNotes.put(nextGuid, relationPoint); } else { normalNotes.put(nextGuid, relationPoint); } } // 連想ノートリストアイテムの最大表示数まで繰り返す for (int i = 0; i < Global.getRensoListItemMaximum(); i++) { // スター付きノートがあれば先に処理する HashMap<String, Integer> tmpMap = new HashMap<String, Integer>(); if (!staredNotes.isEmpty()) { tmpMap = staredNotes; }else if (!normalNotes.isEmpty()) { tmpMap = normalNotes; } // 操作回数が多い順に取り出して連想ノートリストに追加する if (!tmpMap.isEmpty()) { int maxNum = -1; String maxGuid = new String(); for (String nextGuid: tmpMap.keySet()) { int relationPoint = tmpMap.get(nextGuid); // 最大ノート探索する if (relationPoint > maxNum) { maxNum = relationPoint; maxGuid = nextGuid; } } // 次の最大値探索で邪魔なので最大値をHashMapから削除 tmpMap.remove(maxGuid); // 関連度最大のノートがアクティブか確認 Note maxNote = conn.getNoteTable().getNote(maxGuid, true, false, false, false, true); boolean isNoteActive = false; if(maxNote != null) { isNoteActive = maxNote.isActive(); } // 存在していて、かつ関連度0でなければノート情報を取得して連想ノートリストに追加 if (isNoteActive && maxNum > 0) { // Evernoteサムネイルが取得済みか確認。未取得ならサムネイル取得スレッドにキュー if (Global.isConnected) { String thumbnailName = Global.getFileManager().getResDirPath("enThumbnail-" + maxGuid + ".png"); QFile thumbnail = new QFile(thumbnailName); if (!thumbnail.exists()) { // Evernoteサムネイルがファイルとして存在しない QByteArray data = conn.getNoteTable().getENThumbnail(maxGuid); if (data == null) { // Evernoteサムネイル未取得 enThumbnailRunner.addGuid(maxGuid); } } } // スター付きか確認 boolean isStared; isStared = conn.getStaredTable().existNote(currentNoteGuid, maxGuid); QListWidgetItem item = new QListWidgetItem(); RensoNoteListItem myItem = new RensoNoteListItem(maxNote, maxNum, isStared, allPointSum, conn, this); item.setSizeHint(new QSize(0, 90)); this.addItem(item); this.setItemWidget(item, myItem); rensoNoteListItems.put(item, maxGuid); rensoNoteListTrueItems.put(maxGuid, myItem); } else { break; } } } logger.log(logger.EXTREME, "Leaving RensoNoteList.addRensoNoteList"); } // リストのアイテムから対象ノートのguidを取得 public String getNoteGuid(QListWidgetItem item) { return rensoNoteListItems.get(item); } // 関連ノートリストの右クリックメニュー @Override public void contextMenuEvent(QContextMenuEvent event){ logger.log(logger.EXTREME, "Entering RensoNoteList.contextMenuEvent"); if (rensoNotePressedItemGuid == null || rensoNotePressedItemGuid.equals("")) { return; } // STAR, UNSTARがあれば、一度消す List<QAction> menuActions = new ArrayList<QAction>(menu.actions()); if (menuActions.contains(starAction)) { menu.removeAction(starAction); } if (menuActions.contains(unstarAction)) { menu.removeAction(unstarAction); } // 対象アイテムがスター付きなら「UNSTAR」、スター無しなら「STAR」を追加 String currentNoteGuid = parent.getCurrentNoteGuid(); boolean isExist = conn.getStaredTable().existNote(currentNoteGuid, rensoNotePressedItemGuid); if (isExist) { menu.insertAction(excludeNoteAction, unstarAction); } else { menu.insertAction(excludeNoteAction, starAction); } // コンテキストメニューを表示 menu.exec(event.globalPos()); rensoNotePressedItemGuid = null; logger.log(logger.EXTREME, "Leaving RensoNoteList.contextMenuEvent"); } // コンテキストメニューが表示されているかどうか public boolean isContextMenuVisible() { return menu.isVisible(); } // コンテキストメニューが閉じられた時 @SuppressWarnings("unused") private void contextMenuHidden() { for (RensoNoteListItem item : rensoNoteListTrueItems.values()) { item.setDefaultBackground(); } } // ユーザが連想ノートリストのアイテムを選択した時の処理 @SuppressWarnings("unused") private void rensoNoteItemPressed(QListWidgetItem current) { logger.log(logger.HIGH, "Entering RensoNoteList.rensoNoteItemPressed"); rensoNotePressedItemGuid = null; // 右クリックだったときの処理 if (QApplication.mouseButtons().isSet(MouseButton.RightButton)) { rensoNotePressedItemGuid = getNoteGuid(current); } logger.log(logger.HIGH, "Leaving RensoNoteList.rensoNoteItemPressed"); } // Evernoteの関連ノートの取得が完了 @SuppressWarnings("unused") private void enRelatedNotesComplete() { logger.log(logger.HIGH, "Entering RensoNoteList.enRelatedNotesComplete"); Pair<String, List<String>> enRelatedNoteGuidPair = enRelatedNotesRunner.getENRelatedNoteGuids(); // <元ノートguid, 関連ノートguidリスト> if (enRelatedNoteGuidPair == null) { return; } String sourceGuid = enRelatedNoteGuidPair.getFirst(); List<String> enRelatedNoteGuids = enRelatedNoteGuidPair.getSecond(); if (sourceGuid != null && !sourceGuid.equals("") && enRelatedNoteGuids != null) { // Evernote関連ノートがnullでなければ // まずキャッシュに追加 enRelatedNotesCache.put(sourceGuid, enRelatedNoteGuids); if (!enRelatedNoteGuids.isEmpty()) { // Evernote関連ノートが存在していて if (sourceGuid.equals(this.guid)) { // 取得したデータが今開いているノートの関連ノートなら // mergedHistoryにEvernote関連ノートを追加してから再描画 addENRelatedNotes(enRelatedNoteGuids); repaintRensoNoteList(true); } } } logger.log(logger.HIGH, "Leaving RensoNoteList.enRelatedNotesComplete"); } // Evernote関連ノートの関連度情報をmergedHistoryに追加 private void addENRelatedNotes(List<String> relatedNoteGuids) { logger.log(logger.EXTREME, "Entering RensoNoteList.addENRelatedNotes"); // Evernote関連ノート<guid, 関連ポイント> HashMap<String, Integer> enRelatedNotes = new HashMap<String, Integer>(); for (String relatedGuid : relatedNoteGuids) { enRelatedNotes.put(relatedGuid, Global.getENRelatedNotesWeight()); } mergedHistory = mergeHistory(filterHistory(enRelatedNotes), mergedHistory); logger.log(logger.EXTREME, "Leaving RensoNoteList.addENRelatedNotes"); } // Evernoteの関連ノート取得スレッドを終了させる public boolean stopThread() { logger.log(logger.HIGH, "Entering RensoNoteList.stopThread"); if (!enRelatedNotesRunner.addStop()) { logger.log(logger.HIGH, "RensoNoteList.stopThread failed(enRelatedNotesRunner)"); return false; } if (!enThumbnailRunner.addStop()) { logger.log(logger.HIGH, "RensoNoteList.stopThread failed(enThumbnailRunner)"); return false; } logger.log(logger.HIGH, "RensoNoteList.stopThread succeeded"); return true; } public QThread getEnRelatedNotesThread() { return enRelatedNotesThread; } public String getGuid() { return guid; } // ローカルに存在していて、かつアクティブなノートだけを返す private HashMap<String, Integer> filterHistory(HashMap<String, Integer> sourceHistory) { HashMap<String, Integer> dstHistory = new HashMap<String, Integer>(); for (String guid : sourceHistory.keySet()) { if (conn.getNoteTable().exists(guid)) { if (conn.getNoteTable().getNote(guid, false, false, false, false, false).isActive()) { dstHistory.put(guid, sourceHistory.get(guid)); } } } return dstHistory; } /** * Evernoteサムネイルの取得が完了 * * @param guid 現在開いているノートのguid */ @SuppressWarnings("unused") private void enThumbnailComplete(String guid) { logger.log(logger.HIGH, "Entering RensoNoteList.enThumbnailComplete"); for (Map.Entry<String, RensoNoteListItem> e : rensoNoteListTrueItems.entrySet()) { // サムネイル取得が完了したノートが現在の連想ノートリストに表示されていたら再描画 if (guid.equals(e.getKey())) { e.getValue().repaint(); } } logger.log(logger.HIGH, "Leaving RensoNoteList.enThumbnailComplete"); } }