class MFHistory(QWidget): _signal0 = pyqtSignal() _signal1 = pyqtSignal(object) _signal2 = pyqtSignal(object, str) on_lock = pyqtSignal(object) off_lock = pyqtSignal(object) set_hint = pyqtSignal(object, str) set_progress = pyqtSignal(object, float) def __init__(self, parent, base_path, mf_exec): super().__init__(parent) self.parent = parent self.base_path = base_path self.mf_exec = mf_exec # self.filter = None self.worker = None self.stp = None self.time_type, self.time_anchor = 0, 0 self.styleHelper() # _disp = self.w_topbar.hint_label self.on_lock.connect(_disp.getLock) self.off_lock.connect(_disp.releaseLock) self.set_hint.connect(_disp.setHint) self.set_progress.connect(_disp.setProgressHint) pass def styleHelper(self): # set main window layout as grid self.grid = QGridLayout() self.grid.setSpacing(0) self.grid.setContentsMargins(0, 0, 0, 0) # add widget into main layout self.w_topbar = TopbarManager(self) self.w_history_list = MFHistoryList(self, self.base_path) self.grid.addWidget(self.w_topbar, 0, 0) self.grid.addWidget(self.w_history_list, 1, 0) self.setLayout(self.grid) self.setFixedSize(*CFG.SIZE_HISTORY_MAIN()) self.setVisible(False) self.setStyleSheet(CFG.STYLESHEET(self)) pass def hideEvent(self, e): super().hideEvent(e) if not self.isVisible(): self.w_history_list.clear() pass def setFocus(self): self.parent.setFocus() def setFilter(self, _filter): self.filter = _filter self.updateHistory(0, 0) pass def updateHistory(self, type_delta, anchor_delta, relative=False): if not self.isVisible(): return if self.worker and self.worker.isRunning(): self.worker.terminate() self.worker.thread.wait() # mf_type = (self.time_type + type_delta) if type_delta is not None else 0 mf_anchor = (self.time_anchor + anchor_delta) if anchor_delta is not None else 0 if mf_type >= 4: #reset to today mf_type, mf_anchor = 0, 0 relative = False if mf_anchor > 0: return #no future history if relative: self.stp.update_type(mf_type, mf_anchor) else: self.stp = TextStamp(mf_type, mf_anchor) # self.w_topbar.hint_label.setDateHint(self.stp, "(Loading...)") self.worker = MFWorker(self._updateHistory, args=(mf_type, mf_anchor, relative)) self.worker.start() return mf_type def _updateHistory(self, mf_type, mf_anchor, relative): self.items = self.mf_exec.mf_fetch(mf_type, mf_anchor, None, stp=self.stp, locate_flag=True) if relative: self.time_type, self.time_anchor = mf_type, self.stp.diff_time( mf_type) # relative update else: self.time_type, self.time_anchor = mf_type, mf_anchor # iteratively update _postfix = '(filtered)' if self.filter is not None else '' signal_emit(self._signal2, self.w_topbar.hint_label.setDateHint, (self.stp, _postfix)) signal_emit(self._signal1, self.renderHistory, (self.items, )) pass @pyqtSlot(object) def renderHistory(self, items): self.w_history_list.clear() for item in items: (_user, _stp, _text) = item[1] #TODO: match user/timestamp/content/<theme> if (self.filter is None) or self.filter.search(_text): w_item = QListWidgetItem(self.w_history_list) w_item_widget = MFHistoryItem(self, w_item, self.base_path, item) size_hint = QSize(0, w_item_widget.sizeHint().height() + CFG.SIZE_ITEM_MARGIN('MFHistoryItem') ) # do not adjust width w_item.setSizeHint(size_hint) self.w_history_list.addItem(w_item) self.w_history_list.setItemWidget(w_item, w_item_widget) pass pass def showHint(self, hint, show_ms): self.on_lock.emit(self) self.set_hint.emit(self, hint) if show_ms >= 0: QThread.msleep(show_ms) self.off_lock.emit(self) pass def dumpHistory(self, disp): self.on_lock.emit(self) self.set_progress.emit(self, 0.0) # _file = 'MFExport %s.md' % self.stp.hint if Path('~/Desktop').expanduser().is_dir(): _path = Path('~/Desktop', _file).expanduser() else: #bypass some Windows OneDrive _path = Path('~/', _file).expanduser() _total = self.w_history_list.count() with open(str(_path), 'w+', encoding='utf-8') as f: f.write('# Mind Flash Export - %s\n' % self.stp.hint) for i in range(_total): w_item = self.w_history_list.itemWidget( self.w_history_list.item(i)) raw_item, uri = w_item.item, w_item.uri # dump the history item (_user, _time, _) = raw_item _date = datetime.fromtimestamp( int(_time), tz=tzlocal()).strftime('%Y-%m-%d %H:%M:%S') _text = '' _rich_text = w_item.rich_text.copy() for _idx in range(len(w_item.plain_text)): if w_item.plain_text[_idx] == '': if len(_rich_text) == 0: continue _item = _rich_text.pop(0) if _item[1] == 'img': _file = Path(self.base_path, _item[2]) _text += '\n![](%s)\n' % (POSIX(_file)) elif _item[1] == 'file': _file = Path(self.base_path, _item[2]) _text += '\n[%s](%s)\n' % (_file.name, POSIX(_file)) else: _text += '[%s](%s)' % (_item[1], _item[2]) else: _text += '\n' + w_item.plain_text[_idx].strip() pass f.write("**`{user}`** `{date}`\n{text}\n\n------\n".format( date=_date, user=_user, text=_text)) self.set_progress.emit(self, (i + 1) / _total) pass # QThread.msleep(1000) self.off_lock.emit(self) pass pass
class MFGui(QWidget): _signal1 = pyqtSignal(str) _signal2 = pyqtSignal(object, str) def __init__(self): super().__init__() self.mf_exec = mf_exec self.w_todo = MFTodoWidget(self, MF_DIR, sync=False) self.w_history = MFHistory(self, MF_DIR, mf_exec) self.w_editor = MFTextEdit(self, self.w_history, self.w_todo) # set main window layout as grid self.grid = QGridLayout() self.grid.setSpacing(0) self.grid.setContentsMargins(0, 0, 0, 0) self.grid.addWidget(self.w_todo, 0, 0) self.grid.addWidget(self.w_editor, 1, 0) self.grid.setSizeConstraint(QLayout.SetFixedSize) self.setLayout(self.grid) self.resize(self.sizeHint()) # register global shortcuts self.keysFn = KeysReactor(self, 'MFGui') self.registerGlobalKeys() # move window to desktop center qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) # set window style self.setWindowTitle(MF_NAME) self.setWindowIcon(QIcon('./res/icons/pulse_heart.png')) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_InputMethodEnabled) self.setAttribute(Qt.WA_TranslucentBackground, True) self.setContentsMargins(5, 5, 5, 5) self.setGraphicsEffect( QGraphicsDropShadowEffect(blurRadius=5, xOffset=3, yOffset=3)) self.setFocus() self.show() # check update self.worker = MFWorker(self.checkUpdate) self.worker.start() pass def registerGlobalKeys(self): ### Escape / Ctrl+W ### self.keysFn.register(CFG.KEYS_CLOSE(), lambda: self.close()) self.keysFn.register(CFG.KEYS_CLOSE(self), lambda: self.close()) ### Ctrl+L ### self.keysFn.register(CFG.KEYS_TO_EDIT(self), lambda: self.setFocus()) ### Ctrl+F ### def mf_search_binding(): if self.w_history.isVisible(): _topbar = self.w_history.w_topbar _topbar.switch(_topbar.input_box) _topbar.input_box.setFocus() pass self.keysFn.register(CFG.KEYS_SEARCH(self), mf_search_binding) ### Alt+V ### self.keysFn.register( CFG.KEYS_RANGE_SWITCH(), lambda: self.w_history.updateHistory(+1, None, True)) ### Alt+J ### self.keysFn.register(CFG.KEYS_JUMP_FORWARD(), lambda: self.w_history.updateHistory(0, +1)) ### Alt+K ### self.keysFn.register(CFG.KEYS_JUMP_BACKWARD(), lambda: self.w_history.updateHistory(0, -1)) ### Alt+H ### self.keysFn.register(CFG.KEYS_TOGGLE(), lambda: self.w_history.toggleHistoryWidget()) pass def checkUpdate(self): QThread.sleep(5) try: res = url_request.urlopen(MF_STATUS).read().decode('utf-8') res = json.loads(res) _latest = res[0]['name'][1:] if _latest != MF_VERSION and self.w_history.isVisible(): _hint = '<a href="{url}/releases/tag/v{ver}">(v{ver} Available)</a>'.format( url=MF_WEBSITE, ver=_latest) signal_emit(self._signal2, self.w_history.w_topbar.hint_label.setDateHint, (None, _hint)) signal_emit( self._signal1, self.w_history.w_topbar.tool_bar.items['_'].setText, (_hint, )) except Exception as e: print('Check Update Failed: ', e) pass def setFocus(self): self.w_editor.showCaret(force=True) self.w_editor.setFocus() pass pass
class QLabelWrapper(QLabel): def __init__(self, type, text='', pixmap='', alt='', parent=None): super().__init__(text) self.parent = parent self.type = type self.alt = alt if pixmap: self.setPixmap(pixmap) self.styleHelper() pass def styleHelper(self): self.setWordWrap(True) if self.type == 'item_hint': self.setFont(CFG.FONT_ITEM_HINT('MFHistoryItem')) self.setStyleSheet(CFG.STYLE_HINT('MFHistoryItem')) self.setFixedHeight( QFontMetrics(self.font()).height() + CFG.SIZE_HINT_HEIGHT_FIX('MFHistoryItem')) # _user, _date, _time = self.text().split() t_rng = int(_time.split(':')[0]) if t_rng in range(0, 6): _time_color = CFG.COLOR_LATE_NIGHT() elif t_rng in range(6, 12): _time_color = CFG.COLOR_MORNING() elif t_rng in range(12, 18): _time_color = CFG.COLOR_AFTERNOON() else: _time_color = CFG.COLOR_NIGHT() # self.setText(''' <a style="color:{date_color}">{date}</a> <a style="color:{time_color}">{time}</a> <a style="color:{user_color}">@ {user}</a> '''.format( user=_user, user_color=CFG.COLOR_HINT_USER('MFHistoryItem'), date=_date, date_color=CFG.COLOR_HINT_DATE('MFHistoryItem'), time_color=_time_color, time=_time)) self.setToolTip(self.alt) pass elif self.type == 'item_text': self.setFont(CFG.FONT_ITEM_TEXT('MFHistoryItem')) self.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.setTextFormat(Qt.RichText) self.setTextInteractionFlags(Qt.TextBrowserInteraction) self.setOpenExternalLinks(True) self.setStyleSheet(CFG.STYLE_TEXT('MFHistoryItem')) pass elif self.type == 'img_label': self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.setToolTip("View: Left-Click\nCopy: Right-Click") pass elif self.type == 'file_label': _pixmap = self.getFileIcon() _icon_size = CFG.SIZE_FILE_ICON('MFHistoryItem') self.setPixmap( _pixmap.scaledToWidth(_icon_size[0], Qt.SmoothTransformation)) self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) pass else: raise Exception pass pass def getFileIcon(self): # _text = Path(self.alt).name _suffix = Path(self.alt).suffix.upper() _suffix = _suffix[1:] if _suffix else "(Unknown)" _pixmap = QPixmap(120, 120) _pixmap.fill(QColor(0, 0, 0, 0)) _painter = QPainter(_pixmap) _painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing) # draw rounded rect _pen = _painter.pen() _pen.setWidth(2) _painter.setPen(_pen) _painter.drawRoundedRect(QRect(1, 1, 118, 118), 15, 15) # draw suffix text _rect = QRect(8, 10, 108, 35) #100*25 _painter.setPen(QPen(QColor("#0f59a4"))) _painter.setFont(CFG.FONT_ICON_SUFFIX('MFHistoryItem')) _painter.drawText(_rect, Qt.AlignHCenter | Qt.TextSingleLine, _suffix) _painter.setPen(_pen) # draw splitter _painter.drawLine(1, 40, 118, 40) # draw suffix text _rect = QRect(8, 45, 108, 110) #100*65 _painter.setFont(CFG.FONT_ICON_NAME('MFHistoryItem')) _fm = QFontMetrics(_painter.font()) # _elided_text = _fm.elidedText(_text, Qt.ElideMiddle, _rect.width(), Qt.TextWrapAnywhere) _painter.drawText(_rect, Qt.AlignHCenter | Qt.TextWrapAnywhere, _text) del _painter return _pixmap def mousePressEvent(self, ev): if self.type == 'img_label' and ev.buttons() & Qt.RightButton: _clipboard = QApplication.clipboard() if self.alt: _clipboard.setPixmap(self.alt) else: _clipboard.setPixmap(self.pixmap()) try: w_history = self.parent.parent _text = "Image copied." self.worker = MFWorker(w_history.showHint, args=(_text, int(CFG.CFG_HINT_SHOW_MS()))) self.worker.start() except Exception: pass pass elif self.type == 'img_label' and ev.buttons() & Qt.LeftButton: _pixmap = self.alt if self.alt else self.pixmap() w_preview = MFImagePreviewer(self, _pixmap) pass elif self.type == 'file_label' and ev.buttons() & Qt.RightButton: _clipboard = QApplication.clipboard() _mimeData = QMimeData() _mimeData.setUrls([QUrl.fromLocalFile(str(self.alt))]) _clipboard.setMimeData(_mimeData) try: w_history = self.parent.parent _text = "File <u>%s</u> copied." % self.alt.name self.worker = MFWorker(w_history.showHint, args=(_text, int(CFG.CFG_HINT_SHOW_MS()))) self.worker.start() except: pass pass elif self.type == 'file_label' and ev.buttons() & Qt.LeftButton: QDesktopServices.openUrl(QUrl.fromLocalFile(POSIX( self.alt.parent))) pass return super().mousePressEvent(ev) pass
class ToolBarIcon(QLabel): def __init__(self, parent, item): super().__init__('', parent) self.parent = parent self.topbar = parent.parent self.name = item[0] self.attr = item[1] self.press_pos = QPoint(0, 0) self.styleHelper() pass def styleHelper(self): if self.name == '_': _width = CFG.SIZE_TOPBAR_MAIN( )[0] - CFG.SIZE_TOPBAR_ICON()[0] * TOOL_ICON_NUM - ( CFG.SIZE_ICON(self)[0] + 1) #33 for one rightmost icon self.setFixedSize(_width, CFG.SIZE_TOPBAR_MAIN()[1]) self.setStyleSheet('QLabel { border-width: 1px 0px 1px 0px; }') self.setTextFormat(Qt.RichText) self.setTextInteractionFlags(Qt.TextBrowserInteraction) self.setOpenExternalLinks(True) self.callback = None #TODO: impl. MouseReactor return self.icon_name = self.name _icon = QIcon('./res/svg/{}.svg'.format(self.icon_name)).pixmap( QSize(*CFG.SIZE_ICON(self))) self.setPixmap(_icon) self.setToolTip(self.attr['hint']) self.callback = self.__getattribute__(self.attr['func']) self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) if self.attr['pos'] < 0: #leftmost position self.setFixedSize(*CFG.SIZE_TOPBAR_ICON()) self.setStyleSheet('QLabel { border-width: 1px 1px 1px 1px; }') elif self.attr['pos'] > 0: #rightmost position self.setAlignment(Qt.AlignRight | Qt.AlignTop) self.setStyleSheet('QLabel { border-width: 1px 1px 1px 0px; }') else: #middle position self.setFixedSize(*CFG.SIZE_TOPBAR_ICON()) self.setStyleSheet('QLabel { border-width: 1px 1px 1px 0px; }') pass def mousePressEvent(self, e): self.press_pos = e.pos() if e.buttons() & Qt.LeftButton: if self.callback: self.callback() return super().mousePressEvent(e) def mouseMoveEvent(self, e): if (self.callback is None) and (e.buttons() & Qt.LeftButton): _main_body = self.topbar.parent.parent _main_body.move(_main_body.mapToParent(e.pos() - self.press_pos)) # print(self.parent.pos() - self.init_pos) pass elif e.buttons() & Qt.RightButton: pass pass def filterIconEvent(self): self.topbar.switch(self.topbar.input_box) self.topbar.input_box.setFocus() pass def exportIconEvent(self): self.topbar.switch(self.topbar.hint_label) self.worker = MFWorker(self.topbar.parent.dumpHistory, args=(self.topbar.hint_label, )) self.worker.start() pass def historyIconEvent(self): _icons = ['history', 'history-week', 'history-month', 'history-year'] # _idx = self.topbar.parent.updateHistory(+1, None, True) self.icon_name = _icons[_idx] _icon = QIcon('./res/svg/{}.svg'.format(self.icon_name)).pixmap( QSize(*CFG.SIZE_ICON(self))) self.setPixmap(_icon) pass def collapseIconEvent(self): self.topbar.parent.parent.w_editor.toggleHistoryWidget() pass pass