class MainWindow(QMainWindow, Ui_mainWindow): def __init__(self): super().__init__() self.setupUi(self) self.cfgDialog = self.heatMap = None # create on on_cfgAct_triggered restoreWidgetGeo(self, settings['Main'].get('windowGeo')) # setup toolbar bg properties; the second stage is in showEvent self.onExtendTitleBarBgChanged(init=True) self.toolBar.setIconSize(QSize(24, 24) * scaleRatio) # setup TagList width tListW = settings['Main'].getint('tagListWidth') tListW = tListW * scaleRatio if tListW else int(self.width() * 0.2) if not self.isMaximized(): self.splitter.setSizes([tListW, self.width()-tListW]) # setup sort menu self.createSortMenu() self.toolBar.widgetForAction(self.sorAct).setPopupMode(QToolButton.InstantPopup) # Qt Designer doesn't allow us to add widget in toolbar # setup count label countLabel = self.countLabel = QLabel(self.toolBar) countLabel.setObjectName('countLabel') p = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) p.setHorizontalStretch(8) countLabel.setSizePolicy(p) countLabel.setAlignment(Qt.AlignVCenter | Qt.AlignRight) countLabel.setIndent(6) self.toolBar.addWidget(countLabel) # setup search box box = self.searchBox = SearchBox(self.toolBar) p = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) p.setHorizontalStretch(5) box.setSizePolicy(p) box.setMinimumHeight(22 * scaleRatio) box.setMinimumWidth(box.minimumHeight() * 7.5) box.contentChanged.connect(self.nList.setFilterBySearchString) self.toolBar.addWidget(box) spacerWidget = QWidget(self.toolBar) spacerWidget.setFixedSize(2.5 * scaleRatio, 1) self.toolBar.addWidget(spacerWidget) if settings['Main'].getboolean('tagListVisible'): self.tListAct.trigger() else: self.tList.hide() # setup shortcuts searchSc = QShortcut(QKeySequence.Find, self) searchSc.activated.connect(self.searchBox.setFocus) # setup bigger toolbar icons originSz = QSize(24, 24) if scaleRatio > 1.0: for i in [self.cfgAct, self.creAct, self.delAct, self.mapAct, self.sorAct, self.tListAct]: ico = i.icon() ico.addPixmap(ico.pixmap(originSz).scaled(originSz * scaleRatio)) i.setIcon(ico) # setup auto update check if updater.isCheckNeeded(): task = updater.CheckUpdate() QTimer.singleShot(1200, task.start) task.succeeded.connect(self.setUpdateHint) # use lambda here will cause segfault! # delay list loading until main event loop start QTimer.singleShot(0, self.nList.load) def showEvent(self, event): # style polished, we can get correct height of toolbar now self._applyExtendTitleBarBg() self.nList.setFocus() def closeEvent(self, event): settings['Main']['windowGeo'] = saveWidgetGeo(self) tListVisible = self.tList.isVisible() settings['Main']['tagListVisible'] = str(tListVisible) if tListVisible: settings['Main']['tagListWidth'] = str(int(self.splitter.sizes()[0] / scaleRatio)) event.accept() def createSortMenu(self): """Add sort order menu to sorAct.""" menu = QMenu(self) group = QActionGroup(menu) datetime = QAction(self.tr('Date'), group) datetime.name = 'datetime' title = QAction(self.tr('Title'), group) title.name = 'title' length = QAction(self.tr('Length'), group) length.name = 'length' ascDescGroup = QActionGroup(menu) asc = QAction(self.tr('Ascending'), ascDescGroup) asc.name = 'asc' desc = QAction(self.tr('Descending'), ascDescGroup) desc.name = 'desc' for i in [datetime, title, length, None, asc, desc]: if i is None: menu.addSeparator() continue i.setCheckable(True) menu.addAction(i) i.triggered[bool].connect(self.onSortOrderChanged) # restore from settings order = settings['Main']['listSortBy'] locals()[order].setChecked(True) if settings['Main'].getboolean('listReverse'): desc.setChecked(True) else: asc.setChecked(True) self.sorAct.setMenu(menu) def retranslate(self): """Set translation after language changed in ConfigDialog""" setTranslationLocale() self.retranslateUi(self) self.searchBox.retranslate() self.updateCountLabel() def onExtendTitleBarBgChanged(self, init=False): # it's being called by __init__ when init is True ex = settings['Main'].getboolean('extendTitleBarBg') self.toolBar.setProperty('extendTitleBar', ex) type_ = '' if ex: type_ = 'win' if isWin else 'other' self.toolBar.setProperty('titleBarBgType', type_) if not init: self.style().unpolish(self) self.style().polish(self) self._applyExtendTitleBarBg() def _applyExtendTitleBarBg(self): if isWin and settings['Main'].getboolean('extendTitleBarBg'): winDwmExtendWindowFrame(self.winId(), self.toolBar.height()) self.setAttribute(Qt.WA_TranslucentBackground) def onSortOrderChanged(self, checked): name = self.sender().name if name in ['asc', 'desc']: settings['Main']['listReverse'] = str(name == 'desc') elif checked: settings['Main']['listSortBy'] = name self.nList.sort() def toggleTagList(self, checked): self.tList.setVisible(checked) if checked: self.tList.load() else: self.nList.setFilterByTag('') self.tList.clear() settings['Main']['tagListWidth'] = str(int(self.splitter.sizes()[0] / scaleRatio)) def updateCountLabel(self): """Update label that display count of diaries in Main List. 'XX diaries' format is just fine, don't use 'XX diaries,XX results'.""" filtered = (self.nList.modelProxy.filterPattern(0) or self.nList.modelProxy.filterPattern(1)) c = self.nList.modelProxy.rowCount() if filtered else self.nList.originModel.rowCount() self.countLabel.setText(self.tr('%i diaries') % c) def updateCountLabelOnLoad(self): self.countLabel.setText(self.tr('loading...')) def setUpdateHint(self, enabled=None): if enabled is None: enabled = bool(updater.foundUpdate) if enabled: ico = self.cfgAct.icon() self.cfgAct.originIcon = QIcon(ico) # get copy sz = QSize(24, 24) * scaleRatio origin = ico.pixmap(sz) mark = QPixmap(':/toolbar/update-mark.png').scaled(sz) painter = QPainter(origin) painter.drawPixmap(0, 0, mark) painter.end() # this should be called at destruction, but... critical error everywhere? ico.addPixmap(origin) self.cfgAct.setIcon(ico) elif hasattr(self.cfgAct, 'originIcon'): self.cfgAct.setIcon(self.cfgAct.originIcon) del self.cfgAct.originIcon @Slot() def on_cfgAct_triggered(self): """Start config dialog""" try: self.cfgDialog.activateWindow() except (AttributeError, RuntimeError): self.cfgDialog = ConfigDialog(self) self.cfgDialog.langChanged.connect(self.retranslate) self.cfgDialog.bkRestored.connect(self.nList.reload) self.cfgDialog.accepted.connect(self.nList.setDelegateOfTheme) self.cfgDialog.accepted.connect(self.tList.setDelegateOfTheme) self.cfgDialog.extendBgChanged.connect(self.onExtendTitleBarBgChanged) self.cfgDialog.show() @Slot() def on_mapAct_triggered(self): # ratios are from http://www.sonasphere.com/blog/?p=1319 ratio = {QLocale.Chinese: 1, QLocale.English: 4, QLocale.Japanese: 1.5, }.get(QLocale().language(), 1.6) logging.debug('HeatMap got length ratio %s' % ratio) ds = ['0', '< %d' % (200 * ratio), '< %d' % (550 * ratio), '>= %d' % (550 * ratio)] descriptions = [i + ' ' + qApp.translate('HeatMap', '(characters)') for i in ds] def colorFunc(y, m, d, cellColors): data = colorFunc.cached.get((y, m, d), 0) if data == 0: return cellColors[0] elif data < 200 * ratio: return cellColors[1] elif data < 550 * ratio: return cellColors[2] else: return cellColors[3] # iter through model once and cache result. colorFunc.cached = {} model = self.nList.originModel for i in range(model.rowCount()): dt, length = model.index(i, 1).data(), model.index(i, 6).data() year, month, last = dt.split('-') colorFunc.cached[(int(year), int(month), int(last[:2]))] = length try: self.heatMap.activateWindow() except (AttributeError, RuntimeError): self.heatMap = HeatMap(self, objectName='heatMap', font=font.datetime) self.heatMap.closeSc = QShortcut(QKeySequence(Qt.Key_Escape), self.heatMap, activated=self.heatMap.close) self.heatMap.setColorFunc(colorFunc) self.heatMap.sample.setDescriptions(descriptions) self.heatMap.setAttribute(Qt.WA_DeleteOnClose) self.heatMap.resize(self.size()) self.heatMap.move(self.pos()) self.heatMap.setWindowFlags(Qt.Window | Qt.WindowTitleHint) self.heatMap.setWindowTitle('HeatMap') self.heatMap.move(self.pos() + QPoint(12, 12)*scaleRatio) self.heatMap.show()
class MainWindow(QMainWindow, Ui_mainWindow): def __init__(self): super().__init__() self.setupUi(self) self.cfgDialog = self.heatMap = self.ssEditor = None # create on action triggered self.editors = OrderedDict( ) # diaryId => Editor, id of new diary is -1 restoreWidgetGeo(self, settings['Main'].get('windowGeo')) # setup toolbar bg properties; the second stage is in showEvent self.setToolbarProperty() self.toolBar.setIconSize(QSize(24, 24) * scaleRatio) self.diaryList.editAct.triggered.connect(self.startEditor) self.diaryList.gotoAct.triggered.connect(self.onGotoActTriggered) self.diaryList.delAct.triggered.connect(self.deleteDiary) # setup TagList self._tagListAni = QPropertyAnimation(self, 'tagListWidth') self._tagListAni.setEasingCurve(QEasingCurve(QEasingCurve.OutCubic)) self._tagListAni.setDuration(150) self._tagListAni.finished.connect(self.onTagListAniFinished) # setup sort menu menu = QMenu(self) group = QActionGroup(menu) datetime = QAction(self.tr('Date'), group) datetime.name = 'datetime' title = QAction(self.tr('Title'), group) title.name = 'title' length = QAction(self.tr('Length'), group) length.name = 'length' ascDescGroup = QActionGroup(menu) asc = QAction(self.tr('Ascending'), ascDescGroup) asc.name = 'asc' desc = QAction(self.tr('Descending'), ascDescGroup) desc.name = 'desc' for i in [datetime, title, length, None, asc, desc]: if i is None: menu.addSeparator() continue i.setCheckable(True) menu.addAction(i) i.triggered[bool].connect(self.onSortOrderChanged) # restore from settings order = settings['Main']['listSortBy'] locals()[order].setChecked(True) if settings['Main'].getboolean('listReverse'): desc.setChecked(True) else: asc.setChecked(True) self.sorAct.setMenu(menu) self.toolBar.widgetForAction(self.sorAct).setPopupMode( QToolButton.InstantPopup) # setup count label # Qt Designer doesn't allow us to add widget in toolbar p = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) p.setHorizontalStretch(8) spacer1 = QWidget(self.toolBar) spacer1.setSizePolicy(p) self.toolBar.addWidget(spacer1) countLabel = self.countLabel = QLabel(self.toolBar, objectName='countLabel') countLabel.setSizePolicy( QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)) countLabel.setMargin(4 * scaleRatio) self.toolBar.addWidget(countLabel) # setup search box box = self.searchBox = SearchBox(self) p = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) p.setHorizontalStretch(5) box.setSizePolicy(p) box.setMinimumHeight(22 * scaleRatio) box.setMinimumWidth(box.minimumHeight() * 7.5) box.byTitleTextAct.triggered.connect(self._setSearchBy) box.byDatetimeAct.triggered.connect(self._setSearchBy) self._setSearchBy() self.toolBar.addWidget(box) spacer2 = QWidget(self.toolBar) spacer2.setFixedSize(2.5 * scaleRatio, 1) self.toolBar.addWidget(spacer2) if settings['Main'].getboolean('tagListVisible'): self.tListAct.setChecked(True) # will not trigger signal if self.isMaximized(): # Qt will maximize the window after showing... why? QTimer.singleShot( 0, lambda: self.toggleTagList(True, animated=False)) else: self.toggleTagList(True, animated=False) else: self.tagList.hide() # don't use toggleTagList, it will save width # setup shortcuts searchSc = QShortcut(QKeySequence.Find, self) searchSc.activated.connect(self.searchBox.setFocus) # setup bigger toolbar icons if scaleRatio > 1.0: for act, fname in [(self.cfgAct, 'config'), (self.creAct, 'new'), (self.delAct, 'delete'), (self.mapAct, 'heatmap'), (self.sorAct, 'sort'), (self.tListAct, 'tag-list')]: act.setIcon(makeQIcon(':/toolbar/%s.png' % fname)) # setup auto update check if updater.isCheckNeeded(): task = updater.CheckUpdate() QTimer.singleShot(1200, task.start) task.succeeded.connect( self.setUpdateHint) # use lambda here will cause segfault! # delay list loading until main event loop start QTimer.singleShot(0, self.diaryList.load) def showEvent(self, event): # style polished, we can get correct height of toolbar now self._applyExtendTitleBarBg() self.diaryList.setFocus() def closeEvent(self, event): settings['Main']['windowGeo'] = saveWidgetGeo(self) tListVisible = self.tagList.isVisible() settings['Main']['tagListVisible'] = str(tListVisible) if tListVisible: settings['Main']['tagListWidth'] = str( int(self._tagListWidth() / scaleRatio)) def changeEvent(self, event): if event.type() != QEvent.LanguageChange: return super().changeEvent(event) self.retranslateUi(self) self.searchBox.retranslate() self.updateCountLabel() self.tagList.reload() # "All" item def contextMenuEvent(self, event): """Hidden menu.""" menu = QMenu() menu.addAction( QAction(self.tr('Edit Style Sheet'), menu, triggered=self.startStyleSheetEditor)) menu.addAction( QAction(self.tr('Open Data Directory'), menu, triggered=lambda: QDesktopServices.openUrl('file:///' + os. getcwd()))) if mactype.isEnabled(): menu.addAction( QAction(self.tr('Open MacType Config'), menu, triggered=lambda: QDesktopServices.openUrl( 'file:///' + mactype.configPath))) menu.addAction( QAction(self.tr('About Qt'), menu, triggered=qApp.aboutQt)) menu.exec_(event.globalPos()) menu.deleteLater() # disable winEvent hack if PySide version doesn't support it if hasattr(PySide.QtCore, 'MSG') and hasattr(MSG, 'lParam'): def winEvent(self, msg): """Make extended frame draggable (Windows only). This hack is better than receiving MouseMoveEvent and moving the window because it can handle aero snap.""" if msg.message == 0x0084: # WM_NCHITTEST pos = QPoint(msg.lParam & 0xFFFF, msg.lParam >> 16) widget = self.childAt(self.mapFromGlobal(pos)) if widget is self.toolBar or widget is self.countLabel: # qApp.mouseButtons() & Qt.LeftButton doesn't work here; must use win32api return True, 2 # HTCAPTION else: return False, 0 else: return False, 0 # let Qt handle the message def _tagListWidth(self): return self.splitter.sizes()[0] def _setTagListWidth(self, w): sizes = self.splitter.sizes() if sizes[1] == 0: self.splitter.setSizes([w, self.width() - w]) else: sizes[1] = sizes[0] + sizes[1] - w sizes[0] = w self.splitter.setSizes(sizes) def startStyleSheetEditor(self): try: self.ssEditor.activateWindow() except (AttributeError, RuntimeError): self.ssEditor = StyleSheetEditor(self) self.ssEditor.appearanceChanged.connect(self.onAppearanceChanged) self.ssEditor.resize(QSize(600, 550) * scaleRatio) self.ssEditor.show() def setToolbarProperty(self): ex = settings['Main'].getboolean('extendTitleBarBg') self.toolBar.setProperty('extendTitleBar', ex) type_ = '' if ex: if isWin10: type_ = 'win10' # system theme has no border elif isWin: type_ = 'win' else: type_ = 'other' self.toolBar.setProperty('titleBarBgType', type_) if self.isVisible(): # not being called by __init__ refreshStyle(self.toolBar) refreshStyle(self.countLabel) # why is this necessary? self._applyExtendTitleBarBg() def _applyExtendTitleBarBg(self): if settings['Main'].getboolean('extendTitleBarBg'): if isWin: winDwmExtendWindowFrame(self.winId(), top=self.toolBar.height()) self.setAttribute(Qt.WA_TranslucentBackground) if not isWin or not isWin8: eff = NGraphicsDropShadowEffect(5 if isWin7 else 3, self.countLabel) eff.setColor(QColor(Qt.white)) eff.setOffset(0, 0) eff.setBlurRadius((16 if isWin7 else 8) * scaleRatio) self.countLabel.setGraphicsEffect(eff) else: self.countLabel.setGraphicsEffect(None) def onSortOrderChanged(self, checked): name = self.sender().name if name in ['asc', 'desc']: settings['Main']['listReverse'] = str(name == 'desc') elif checked: settings['Main']['listSortBy'] = name self.diaryList.sort() def toggleTagList(self, show, animated=True): if show: if self._tagListAni.state() == QAbstractAnimation.Running: self._tagListAni.stop() else: self.tagList.load() self.tagList.show() # minus 1 to make animation direction check correct tListW = settings['Main'].getint('tagListWidth') tListW = tListW * scaleRatio if tListW else int(self.width() * 0.2) if animated: self._tagListAni.hiding = False self._tagListAni.setStartValue( max(self._tagListWidth(), self.tagList.minimumSize().width())) self._tagListAni.setEndValue(tListW) self._tagListAni.start() else: self._setTagListWidth(tListW) else: if self._tagListAni.state() == QAbstractAnimation.Running: self._tagListAni.stop() else: settings['Main']['tagListWidth'] = str( int(self._tagListWidth() / scaleRatio)) if animated: self._tagListAni.hiding = True self._tagListAni.setStartValue(self._tagListWidth()) self._tagListAni.setEndValue( self.tagList.minimumSize().width()) self._tagListAni.start() else: self.tagList.hide() def updateCountLabel(self): """Update label that display count of diaries in Main List. 'XX diaries' format is just fine, don't use 'XX diaries,XX results'.""" self.countLabel.setText( self.tr('%i diaries') % self.diaryList.modelProxy.rowCount()) def updateCountLabelOnLoad(self): self.countLabel.setText(self.tr('loading...')) def setUpdateHint(self, enabled=None): if enabled is None: enabled = bool(updater.foundUpdate) if enabled: ico = self.cfgAct.icon() self.cfgAct.originIcon = QIcon(ico) # save copy self.cfgAct.setIcon(markIcon(ico, QSize(24, 24)), ':/toolbar/update-mark.png') elif hasattr(self.cfgAct, 'originIcon'): self.cfgAct.setIcon(self.cfgAct.originIcon) del self.cfgAct.originIcon def _setSearchBy(self): sen = self.sender() if sen: self.searchBox.contentChanged.disconnect() if sen == self.searchBox.byTitleTextAct or sen is None: self.searchBox.contentChanged.connect( self.diaryList.setFilterBySearchString) else: self.searchBox.contentChanged.connect( self.diaryList.setFilterByDatetime) def deleteDiary(self): indexes = self.diaryList.selectedIndexes() if not indexes: return msg = QMessageBox(self) okBtn = msg.addButton(qApp.translate('Dialog', 'Delete'), QMessageBox.AcceptRole) msg.setIcon(QMessageBox.Question) msg.addButton(qApp.translate('Dialog', 'Cancel'), QMessageBox.RejectRole) msg.setWindowTitle(self.tr('Delete diaries')) msg.setText(self.tr('Selected diaries will be deleted permanently!')) msg.exec_() msg.deleteLater() if msg.clickedButton() == okBtn: for i in indexes: db.delete(i.data()) for r in reversed(sorted(i.row() for i in indexes)): self.diaryList.model().removeRow(r) self.tagList.reload() # tags might changed def startEditor(self, idx=None): dic = self.diaryList.getDiaryDict(idx or self.diaryList.currentIndex()) id_ = dic['id'] if id_ in self.editors: self.editors[id_].activateWindow() else: e = Editor(dic) self._setEditorStaggerPos(e) self.editors[id_] = e e.closed.connect(self.onEditorClose) pre, next_ = lambda: self._editorMove( -1), lambda: self._editorMove(1) e.preSc.activated.connect(pre) e.quickPreSc.activated.connect(pre) e.nextSc.activated.connect(next_) e.quickNextSc.activated.connect(next_) e.show() def startEditorNew(self): if -1 in self.editors: self.editors[-1].activateWindow() else: e = Editor({'id': -1}) self._setEditorStaggerPos(e) self.editors[-1] = e e.closed.connect(self.onEditorClose) e.show() def _setEditorStaggerPos(self, editor): if self.editors: lastOpenEditor = list(self.editors.values())[-1] pos = lastOpenEditor.pos() + QPoint(16, 16) * scaleRatio # can't check available screen space because of bug in pyside editor.move(pos) def _editorMove(self, step): if len(self.editors) > 1: return id_ = list(self.editors.keys())[0] editor = self.editors[id_] if editor.needSave(): return model = self.diaryList.modelProxy idx = model.match(model.index(0, 0), 0, id_, flags=Qt.MatchExactly) if len(idx) != 1: return row = idx[0].row( ) # the row of the caller (Editor) 's diary in proxy model if ((step == -1 and row == 0) or (step == 1 and row == model.rowCount() - 1)): return newIdx = model.index(row + step, 0) self.diaryList.clearSelection() self.diaryList.setCurrentIndex(newIdx) dic = self.diaryList.getDiaryDict(newIdx) editor.fromDiaryDict(dic) self.editors[dic['id']] = self.editors.pop(id_) def onEditorClose(self, id_, needSave): """Write editor's data to model and database, and destroy editor""" editor = self.editors[id_] new = id_ == -1 if needSave: qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) dic = editor.toDiaryDict() if not new and not editor.tagModified: # let database skip heavy tag update operation dic['tags'] = None row = self.diaryList.originModel.saveDiary(dic) self.diaryList.clearSelection() self.diaryList.setCurrentIndex( self.diaryList.modelProxy.mapFromSource( self.diaryList.originModel.index(row, 0))) if new: self.updateCountLabel() if editor.tagModified: self.tagList.reload() qApp.restoreOverrideCursor() editor.deleteLater() del self.editors[id_] def onAppearanceChanged(self): self.diaryList.setupTheme() self.tagList.setupTheme() self.setToolbarProperty() def onGotoActTriggered(self): """Scroll the list to the original position (unfiltered) of an entry.""" if self.searchBox.text(): self.searchBox.clear() if self.tagList.selectedIndexes(): self.tagList.setCurrentRow(0) self.diaryList.scrollTo(self.diaryList.currentIndex(), QListView.PositionAtCenter) def onTagListAniFinished(self): if self._tagListAni.hiding: self.tagList.hide() self.tagList.clear() @Slot() def on_cfgAct_triggered(self): """Start config dialog""" try: self.cfgDialog.activateWindow() except (AttributeError, RuntimeError): self.cfgDialog = ConfigDialog(self) self.cfgDialog.diaryChanged.connect(self.diaryList.reload) self.cfgDialog.appearanceChanged.connect(self.onAppearanceChanged) self.cfgDialog.show() @Slot() def on_mapAct_triggered(self): # some languages have more info in single character # ratios are from http://www.sonasphere.com/blog/?p=1319 ratio = { QLocale.Chinese: 1, QLocale.English: 4, QLocale.Japanese: 1.5, }.get(QLocale().language(), 1.6) logging.debug('HeatMap got length ratio %s' % ratio) ds = [ '0', '< %d' % (200 * ratio), '< %d' % (550 * ratio), '>= %d' % (550 * ratio) ] descriptions = [ i + ' ' + qApp.translate('HeatMap', '(characters)') for i in ds ] def colorFunc(data, cellColors): if data == 0: return cellColors[0] elif data < 200 * ratio: return cellColors[1] elif data < 550 * ratio: return cellColors[2] else: return cellColors[3] # iter through model once and cache result. cached = {} for diary in self.diaryList.originModel.getAll(): dt, length = diary[DiaryModel.DATETIME], diary[DiaryModel.LENGTH] year, month, last = dt.split('-') cached[(int(year), int(month), int(last[:2]))] = length try: self.heatMap.activateWindow() except (AttributeError, RuntimeError): self.heatMap = HeatMap(self, objectName='heatMap', font=font.datetime) self.heatMap.closeSc = QShortcut(QKeySequence(Qt.Key_Escape), self.heatMap, activated=self.heatMap.close) self.heatMap.setColorFunc(colorFunc) self.heatMap.setDataFunc(lambda y, m, d: cached.get((y, m, d), 0)) self.heatMap.sample.setDescriptions(descriptions) self.heatMap.setAttribute(Qt.WA_DeleteOnClose) self.heatMap.resize(self.size()) self.heatMap.setWindowFlags(Qt.Window | Qt.WindowTitleHint) self.heatMap.setWindowTitle(self.tr('Heat Map')) self.heatMap.move(self.pos() + QPoint(12, 12) * scaleRatio) self.heatMap.show() tagListWidth = Property(int, _tagListWidth, _setTagListWidth)