├── README.md ├── background.png ├── besteam ├── __init__.py ├── im │ ├── __init__.py │ └── quick_panel │ │ ├── __init__.py │ │ ├── canvas.py │ │ ├── layout_editor.py │ │ ├── selectwidgets.ui │ │ ├── services.py │ │ └── widgets │ │ ├── __init__.py │ │ ├── bookmark.ui │ │ ├── calendar.py │ │ ├── desktop_icon.py │ │ ├── machine_load.py │ │ ├── quick_access.py │ │ ├── shortcut.ui │ │ ├── textpad.py │ │ ├── todo_backend.py │ │ ├── todo_editor.ui │ │ ├── todo_list.py │ │ └── todolist.ui └── utils │ ├── __init__.py │ ├── globalkey.py │ ├── kdeglobalkey.py │ ├── settings.py │ ├── sql.py │ └── winglobalkey.py ├── clean.py ├── compile_files.py ├── images ├── angelfish.png ├── change_background.png ├── change_layout.png ├── close.png ├── configure.png ├── delete.png ├── edit-rename.png ├── edit.png ├── folder-documents.png ├── folder-image.png ├── folder-sound.png ├── hello.png ├── httpurl.png ├── insert-link.png ├── new.png ├── reset.png ├── select_widgets.png ├── tetrix.png ├── unknown.png ├── user-home.png └── weather │ ├── weather-clear.png │ ├── weather-clouds.png │ ├── weather-freezing-rain.png │ ├── weather-many-clouds.png │ ├── weather-none-available.png │ ├── weather-showers-day.png │ ├── weather-showers.png │ ├── weather-snow-rain.png │ ├── weather-snow.png │ └── weather-storm.png ├── license.txt ├── quickpanel.qrc ├── start_quickpanel.py ├── tetrix.py └── wc.py /README.md: -------------------------------------------------------------------------------- 1 | Quickpanel is a small popup desktop which supports widgets and desktop icons. When activated, it is shown at the center of screen. 2 | 3 | [screenshort](http://hgoldfish.com/static/quickpanel_screenshot.png) 4 | 5 | Quickpanel can be run in KDE desktop and windows. 6 | 7 | Use it in KDE Desktop: 8 | 9 | $ zypper install python-qt5-devel python-kde5 10 | 11 | $ git clone git@github.com:hgoldfish/quickpanel.git 12 | $ cd quickpanel 13 | $ python3 compile_files.py 14 | $ python3 start_quickpanel.py 15 | 16 | After quickpanel started, a system tray icon shown at the right bottom of your desktop. Click it or press Alt + `, shows the quickpanel. 17 | 18 | -------------------------------------------------------------------------------- /background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/background.png -------------------------------------------------------------------------------- /besteam/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/besteam/__init__.py -------------------------------------------------------------------------------- /besteam/im/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/besteam/im/__init__.py -------------------------------------------------------------------------------- /besteam/im/quick_panel/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import functools 3 | import logging 4 | import ctypes 5 | from PyQt5.QtCore import QAbstractListModel, QModelIndex, QRect, QTimer, Qt, QStandardPaths 6 | from PyQt5.QtGui import QBrush, QColor, QImage, QPainter, QPen, QIcon, QDesktopServices 7 | from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox, \ 8 | QSizePolicy, QToolBar, QWidget, QAction, QHBoxLayout, \ 9 | QVBoxLayout, QLabel, QFileDialog 10 | 11 | from .layout_editor import LayoutEditor 12 | from .canvas import Canvas 13 | from .services import WidgetConfigure, QuickPanelDatabase 14 | 15 | __all__ = ["QuickPanel"] 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | def moveToCenter(window): 20 | r = window.geometry() 21 | r.moveCenter(QApplication.instance().desktop().screenGeometry().center()) 22 | window.setGeometry(r) 23 | 24 | class WidgetManager: 25 | """QuickPanel类的一部分,分出来方便阅读与理解。主要功能是管理快捷面板的部件""" 26 | 27 | def initWidgets(self): 28 | self.widgets = [] 29 | 30 | from besteam.im.quick_panel.widgets.todo_list import TodoListWidget 31 | from besteam.im.quick_panel.widgets.quick_access import QuickAccessWidget 32 | from besteam.im.quick_panel.widgets.textpad import TextpadWidget 33 | from besteam.im.quick_panel.widgets.desktop_icon import DesktopIconWidget 34 | from besteam.im.quick_panel.widgets.machine_load import MachineLoadWidget 35 | from besteam.im.quick_panel.widgets.calendar import CalendarWidget 36 | self.registerWidget("bc8ada4f-50b8-49f7-917a-da163b6763e9", self.tr("待办事项列表"), \ 37 | self.tr("用于纪录当前正在进行中的待办事项。"), \ 38 | TodoListWidget) 39 | self.registerWidget("be6c197b-0181-47c0-a9fc-6a1fe5f1b3e6", self.tr("Besteam快捷方式"), \ 40 | self.tr("快捷启动Besteam的附加工具。"), \ 41 | QuickAccessWidget) 42 | self.registerWidget("45d1ee54-f9bd-435e-93cf-b46a05b56514", self.tr("文本框"), \ 43 | self.tr("简单纪录文本。退出Besteam被丢弃。"), \ 44 | TextpadWidget) 45 | self.registerWidget("dd6afcb0-e223-4156-988d-20f20266c6f0", self.tr("桌面快捷方式"), \ 46 | self.tr("启动外部程序。"), \ 47 | DesktopIconWidget) 48 | self.registerWidget("b0b6b9eb-aec0-4fe5-bfd0-d4d317fdd547", self.tr("CPU使用率"), \ 49 | self.tr("以折线的形式显示一段时间内的CPU使用率。"), \ 50 | MachineLoadWidget) 51 | self.registerWidget("d94db588-663b-4a6f-b935-4ca9ff283c75", self.tr("现在时间"),\ 52 | self.tr("显示当前时间。"), \ 53 | CalendarWidget) 54 | logger.debug("All builtin widgets have been registered.") 55 | 56 | def finalize(self): 57 | self.unregisterWidget("bc8ada4f-50b8-49f7-917a-da163b6763e9") 58 | self.unregisterWidget("be6c197b-0181-47c0-a9fc-6a1fe5f1b3e6") 59 | self.unregisterWidget("45d1ee54-f9bd-435e-93cf-b46a05b56514") 60 | self.unregisterWidget("dd6afcb0-e223-4156-988d-20f20266c6f0") 61 | self.unregisterWidget("35e3397b-500e-4bcc-97d9-4f84997e1e46") 62 | self.unregisterWidget("b0b6b9eb-aec0-4fe5-bfd0-d4d317fdd547") 63 | self.unregisterWidget("d94db588-663b-4a6f-b935-4ca9ff283c75") 64 | for widget in list(self.widgets): 65 | self.unregisterWidget(widget.id) 66 | logger.debug("All builtin widgets have been unregistered.") 67 | 68 | def getAllWidgets(self): 69 | return self.widgets 70 | 71 | def registerWidget(self, id, name, description, factory): 72 | widget = WidgetConfigure() 73 | config = self.db.getWidgetConfig(id) 74 | if config is None: 75 | config = {} 76 | config["id"] = id 77 | config["left"] = 15 78 | config["top"] = 10 79 | config["width"] = 10 80 | config["height"] = 10 81 | config["enabled"] = False 82 | self.db.saveWidgetConfig(config) 83 | widget.rect = QRect(15, 10, 10, 10) 84 | widget.enabled = False 85 | else: 86 | widget.rect = QRect(config["left"], config["top"], config["width"], config["height"]) 87 | widget.enabled = config["enabled"] 88 | widget.id = id 89 | widget.name = name 90 | widget.description = description 91 | widget.factory = factory 92 | widget.widget = None 93 | self.widgets.append(widget) 94 | if widget.enabled: 95 | self._enableWidget(widget, False) 96 | 97 | def unregisterWidget(self, widgetId): 98 | i = 0 99 | for widget in self.widgets: 100 | if widget.id == widgetId: 101 | if widget.widget is not None: 102 | assert widget.enabled 103 | self._disableWidget(widget, False) 104 | self.widgets.pop(i) 105 | break 106 | i += 1 107 | 108 | def enableWidget(self, widgetId): 109 | "根据部件的ID启用部件。也是将它显示在快捷面板上面。" 110 | for widget in self.widgets: 111 | if widget.id == widgetId: 112 | self._enableWidget(widget, True) 113 | break 114 | 115 | def disableWidget(self, widgetId): 116 | "根据部件的ID禁用部件。让它从快捷面板中删除。" 117 | for widget in self.widgets: 118 | if widget.id == widgetId: 119 | self._disableWidget(widget, True) 120 | break 121 | 122 | def _enableWidget(self, widget, syncToDatabase = True): 123 | if widget.widget is not None: 124 | return 125 | try: 126 | widget.widget = widget.factory(self.canvas) 127 | except: 128 | logger.exception("error occured while call widget's factory function.") 129 | return 130 | widget.enabled = True 131 | self.canvas.showWidget(widget) 132 | if syncToDatabase: 133 | self.db.setWidgetEnabled(widget.id, True) 134 | 135 | def _disableWidget(self, widget, syncToDatabase = True): 136 | if widget.widget is None: 137 | return 138 | if hasattr(widget.widget, "finalize"): 139 | try: 140 | widget.widget.finalize() 141 | except: 142 | logger.exception("error occured while closing widget.") 143 | self.canvas.closeWidget(widget) 144 | widget.widget.hide() 145 | widget.widget.setParent(None) 146 | widget.widget = None 147 | widget.enabled = False 148 | if syncToDatabase: 149 | self.db.setWidgetEnabled(widget.id, False) 150 | 151 | def selectWidgets(self): 152 | self.layoutEditor.selectWidgets() 153 | 154 | def resetDefaultLayout(self): 155 | self.layoutEditor.resetLayout() 156 | 157 | 158 | class QuickPanel(QWidget, WidgetManager): 159 | """ 160 | 一个快捷面板。类似于KDE的桌面。只不过功能会简单些。主要的方法有: 161 | addQuickAccessShortcut() 供插件添加一个系统快捷方式。 162 | removeQuickAccessShortcut() 删除插件添加的系统快捷方式。 163 | toggle() 如果快捷面板已经显示就隐藏它。如果处于隐藏状态则显示它。 164 | showAndGetFocus() 显示快捷面板并且将焦点放置于常用的位置。 165 | registerWidget() 注册部件 166 | unregisterWidget() 反注册部件 167 | """ 168 | 169 | def __init__(self, platform): 170 | QWidget.__init__(self, None, Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) 171 | self.setWindowModality(Qt.ApplicationModal) 172 | self.platform = platform 173 | self.db = QuickPanelDatabase(platform.databaseFile) 174 | self.createActions() 175 | self.createControls() 176 | self.loadSettings() 177 | self.makeConnections() 178 | #Besteam系统快捷方式作为QuickPanel提供的一个服务,必须定义在这里 179 | #虽然部件可能没有运行。QuickPanel也应该记住其它插件添加的快捷方式,以便在 180 | #用户添加QuickAccessWidget之后,可以显示所有的系统快捷方式 181 | self.quickAccessModel = QuickAccessModel() 182 | 183 | def createActions(self): 184 | self.actionChangeBackground = QAction(self) 185 | self.actionChangeBackground.setIcon(QIcon(":/images/change_background.png")) 186 | self.actionChangeBackground.setText(self.tr("Change &Background")) 187 | self.actionChangeBackground.setIconText(self.tr("Background")) 188 | self.actionChangeBackground.setToolTip(self.tr("Change Background")) 189 | self.actionClose = QAction(self) 190 | self.actionClose.setIcon(QIcon(":/images/close.png")) 191 | self.actionClose.setText(self.tr("&Close")) 192 | self.actionClose.setIconText(self.tr("Close")) 193 | self.actionClose.setToolTip(self.tr("Close")) 194 | self.actionChangeLayout = QAction(self) 195 | self.actionChangeLayout.setIcon(QIcon(":/images/change_layout.png")) 196 | self.actionChangeLayout.setText(self.tr("Change &Layout")) 197 | self.actionChangeLayout.setIconText(self.tr("Layout")) 198 | self.actionChangeLayout.setToolTip(self.tr("Change Layout")) 199 | self.actionChangeLayout.setCheckable(True) 200 | self.actionSelectWidgets = QAction(self) 201 | self.actionSelectWidgets.setIcon(QIcon(":/images/select_widgets.png")) 202 | self.actionSelectWidgets.setText(self.tr("&Select Widgets")) 203 | self.actionSelectWidgets.setIconText(self.tr("Widgets")) 204 | self.actionSelectWidgets.setToolTip(self.tr("Select Widgets")) 205 | self.actionResetBackground = QAction(self) 206 | self.actionResetBackground.setIcon(QIcon(":/images/reset.png")) 207 | self.actionResetBackground.setText(self.tr("&Reset Background")) 208 | self.actionResetBackground.setIconText(self.tr("Reset")) 209 | self.actionResetBackground.setToolTip(self.tr("Reset Background")) 210 | self.actionResetDefaultLayout = QAction(self) 211 | self.actionResetDefaultLayout.setIcon(QIcon(":/images/reset.png")) 212 | self.actionResetDefaultLayout.setText(self.tr("Reset &Layout")) 213 | self.actionResetDefaultLayout.setIconText(self.tr("Reset")) 214 | self.actionResetDefaultLayout.setToolTip(self.tr("Reset Layout")) 215 | 216 | def createControls(self): 217 | self.toolBarMain = QToolBar(self) 218 | self.toolBarMain.addAction(self.actionChangeBackground) 219 | self.toolBarMain.addAction(self.actionResetBackground) 220 | self.toolBarMain.addAction(self.actionChangeLayout) 221 | self.toolBarMain.addAction(self.actionClose) 222 | self.toolBarMain.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 223 | self.toolBarMain.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 224 | self.toolBarLayout = QToolBar(self) 225 | self.toolBarLayout.addAction(self.actionSelectWidgets) 226 | self.toolBarLayout.addAction(self.actionResetDefaultLayout) 227 | self.toolBarLayout.addAction(self.actionChangeLayout) 228 | self.toolBarLayout.addAction(self.actionClose) 229 | self.toolBarLayout.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 230 | self.toolBarLayout.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 231 | 232 | self.canvas = Canvas(self) 233 | self.layoutEditor = LayoutEditor(self) 234 | 235 | self.lblTitle = QLabel(self) 236 | self.lblTitle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) 237 | self.setLayout(QVBoxLayout()) 238 | self.layoutTop = QHBoxLayout() 239 | self.layoutTop.addWidget(self.lblTitle) 240 | self.layoutTop.addWidget(self.toolBarMain) 241 | self.layoutTop.addWidget(self.toolBarLayout) 242 | self.toolBarLayout.hide() 243 | self.layout().addLayout(self.layoutTop) 244 | self.layout().addWidget(self.canvas) 245 | self.layout().addWidget(self.layoutEditor) 246 | self.layoutEditor.hide() 247 | 248 | def loadSettings(self): 249 | settings = self.platform.getSettings() 250 | filepath = settings.value("background", "background.png") 251 | if not os.path.exists(filepath): 252 | filepath = os.path.join(os.path.dirname(__file__), filepath) 253 | if not os.path.exists(filepath): 254 | return 255 | image = QImage(filepath) 256 | self._makeBackground(image) 257 | 258 | def makeConnections(self): 259 | self.actionClose.triggered.connect(self.close) 260 | self.actionChangeLayout.triggered.connect(self.toggleLayoutEditor) 261 | self.actionChangeBackground.triggered.connect(self.changeBackground) 262 | self.actionResetBackground.triggered.connect(self.useDefaultBackground) 263 | self.actionSelectWidgets.triggered.connect(self.selectWidgets) 264 | self.actionResetDefaultLayout.triggered.connect(self.resetDefaultLayout) 265 | QApplication.instance().focusChanged.connect(self.onWindowFocusChanged) 266 | 267 | def paintEvent(self, event): 268 | painter = QPainter(self) 269 | painter.drawImage(event.rect(), self._background_image, event.rect()) 270 | 271 | def keyPressEvent(self, event): 272 | QWidget.keyPressEvent(self, event) 273 | if not event.isAccepted() and event.key() == Qt.Key_Escape: 274 | if self.layoutEditor.isVisible(): 275 | self.leaveLayoutEditor() 276 | self.actionChangeLayout.setChecked(False) 277 | else: 278 | self.close() 279 | 280 | def onWindowFocusChanged(self, old, new): 281 | "实现类似于Qt.Popup那样点击其它窗口就立即关闭本窗口的效果。" 282 | if self.isVisible() and not self.isActiveWindow(): 283 | self.close() 284 | 285 | def showEvent(self, event): 286 | settings = self.platform.getSettings() 287 | key = settings.value("globalkey", "Alt+`") 288 | if key is not None: 289 | if os.name == "nt": #在Windows系统下,Meta键习惯叫Win键 290 | key = key.replace("Meta", "Win") 291 | title = self.tr("提示:在任何位置按{0}打开快捷面板。").format(key) 292 | self.lblTitle.setText('{0}'.format(title)) 293 | else: 294 | title = self.tr("快捷面板") 295 | self.lblTitle.setText('{0}'.format(title)) 296 | 297 | #如果有时候运行全屏程序,快捷面板的位置就会发生改变 298 | self._makeBackground(self._background_image) 299 | moveToCenter(self) 300 | self.canvas.positWidgets() 301 | QWidget.showEvent(self, event) 302 | 303 | def showAndGetFocus(self): 304 | if not self.isVisible(): 305 | self.show() 306 | if self.windowState() & Qt.WindowMinimized: 307 | self.setWindowState(self.windowState() ^ Qt.WindowMinimized) 308 | self.raise_() 309 | if os.name == "nt": 310 | ctypes.windll.user32.BringWindowToTop(int(self.winId())) 311 | ctypes.windll.user32.SwitchToThisWindow(int(self.winId()), 1) 312 | self.activateWindow() 313 | 314 | def toggle(self): 315 | if self.isVisible(): 316 | self.hide() 317 | else: 318 | self.showAndGetFocus() 319 | 320 | def addQuickAccessShortcut(self, name, icon, callback): 321 | """ 322 | 添加一个快捷方式。有一个widget专门用于显示Besteam内部各种工具的快捷方式。 323 | name 快捷方式的名字 324 | icon 快捷方式的图标 325 | callback 当用户点击快捷方式的时候调用的回调函数。不会传入任何参数。 326 | """ 327 | self.quickAccessModel.addShortcut(name, icon, callback) 328 | 329 | def removeQuickAccessShortcut(self, name): 330 | """删除一个系统快捷方式。参数name是快捷方式的名字。""" 331 | self.quickAccessModel.removeShortcut(name) 332 | 333 | def enterLayoutEditor(self): 334 | self.layoutEditor.show() 335 | self.canvas.hide() 336 | self.toolBarLayout.show() 337 | self.toolBarMain.hide() 338 | self.layoutEditor.beginEdit(self.widgets) 339 | 340 | def leaveLayoutEditor(self): 341 | self.layoutEditor.hide() 342 | self.canvas.show() 343 | self.toolBarLayout.hide() 344 | self.toolBarMain.show() 345 | changedWidgets = self.layoutEditor.saveLayout(self.widgets) 346 | for widget in changedWidgets: 347 | conf = {} 348 | conf["left"] = widget.rect.left() 349 | conf["top"] = widget.rect.top() 350 | conf["width"] = widget.rect.width() 351 | conf["height"] = widget.rect.height() 352 | conf["enabled"] = widget.enabled 353 | conf["id"] = widget.id 354 | self.db.saveWidgetConfig(conf) 355 | if widget.enabled: 356 | self._enableWidget(widget, False) 357 | else: 358 | self._disableWidget(widget, False) 359 | self.canvas.positWidgets(True) 360 | self.layoutEditor.endEdit() 361 | 362 | def toggleLayoutEditor(self, checked): 363 | if checked: 364 | self.enterLayoutEditor() 365 | else: 366 | self.leaveLayoutEditor() 367 | 368 | def changeBackground(self): 369 | filename, selectedFilter = QFileDialog.getOpenFileName(self, self.tr("Change Background"), \ 370 | QStandardPaths.writableLocation(QStandardPaths.PicturesLocation), \ 371 | self.tr("Image Files (*.png *.gif *.jpg *.jpeg *.bmp *.mng *ico)")) 372 | if not filename: 373 | return 374 | image = QImage(filename) 375 | if image.isNull(): 376 | QMessageBox.information(self, self.tr("Change Background"), \ 377 | self.tr("不能读取图像文件,请检查文件格式是否正确,或者图片是否已经损坏。")) 378 | return 379 | if image.width() < 800 or image.height() < 600: 380 | answer = QMessageBox.information(self, self.tr("Change Background"), \ 381 | self.tr("不建议设置小于800x600的图片作为背景图案。如果继续,可能会使快捷面板显示错乱。是否继续?"), 382 | QMessageBox.Yes | QMessageBox.No, 383 | QMessageBox.No) 384 | if answer == QMessageBox.No: 385 | return 386 | self._makeBackground(image) 387 | moveToCenter(self) 388 | self.canvas.positWidgets() 389 | self.update() 390 | with self.platform.getSettings() as settings: 391 | settings.setValue("background", filename) 392 | 393 | def useDefaultBackground(self): 394 | filename = "background.png" 395 | if not os.path.exists(filename): 396 | filename = os.path.join(os.path.dirname(__file__), filename) 397 | if os.path.exists(filename): 398 | image = QImage(filename) 399 | if not image.isNull(): 400 | self._makeBackground(image) 401 | moveToCenter(self) 402 | self.canvas.positWidgets() 403 | self.update() 404 | settings = self.platform.getSettings() 405 | settings.remove("background") 406 | 407 | def _makeBackground(self, image): 408 | desktopSize = QApplication.desktop().screenGeometry(self).size() 409 | if desktopSize.width() < image.width() or desktopSize.height() < image.height(): 410 | self._background_image = image.scaled(desktopSize, Qt.KeepAspectRatio, Qt.SmoothTransformation) 411 | else: 412 | self._background_image = image 413 | self.resize(self._background_image.size()) 414 | 415 | def runDialog(self, *args, **kwargs): 416 | """ 417 | 在QuickPanel中显示一个对话框。主要是为了避免对话框显示的时候,快捷面板会隐藏。 418 | 接受两种形式的参数,其中d是对话框: 419 | runDialog(d, d.exec_) 420 | runDialog(d.exec_, *args, **kwargs) 421 | 建议使用第二种 422 | """ 423 | if isinstance(args[0], QDialog): 424 | return self._runDialog2(args[0], args[1]) 425 | else: 426 | callback, args = args[0], args[1:] 427 | return self._runDialog3(callback, args, kwargs) 428 | 429 | def _runDialog2(self, d, callback): 430 | return self._runDialog(d, self.canvas, callback) 431 | 432 | def _runDialog3(self, callback, args, kwargs): 433 | d = callback.__self__ 434 | f = functools.partial(callback, *args, **kwargs) 435 | return self._runDialog(d, self.canvas, f) 436 | 437 | def _runDialog(self, d, container, callback): 438 | shutter = ShutterWidget(container) 439 | newPaintEvent = functools.partial(self._dialog_paintEvent, d) 440 | oldPaintEvent = d.paintEvent 441 | d.paintEvent = newPaintEvent 442 | r = d.geometry() 443 | r.moveCenter(container.rect().center()) 444 | d.setGeometry(r) 445 | d.setWindowFlags(Qt.Widget) 446 | d.setParent(container) 447 | d.setFocus(Qt.OtherFocusReason) 448 | try: 449 | shutter.show() 450 | d.raise_() 451 | return callback() 452 | finally: 453 | d.paintEvent = oldPaintEvent 454 | shutter.close() 455 | shutter.setParent(None) 456 | 457 | def _dialog_paintEvent(self, d, event): 458 | QDialog.paintEvent(d, event) 459 | pen = QPen() 460 | pen.setWidth(2) 461 | pen.setColor(QColor(200, 200, 200)) 462 | rect = d.rect() 463 | rect = rect.adjusted(0, 0, -1, -1) 464 | painter = QPainter(d) 465 | painter.setRenderHint(QPainter.Antialiasing, True) 466 | painter.setPen(pen) 467 | painter.setOpacity(0.8) 468 | painter.setBrush(QBrush(QColor(Qt.white))) 469 | painter.drawRoundedRect(rect, 15, 15) 470 | 471 | 472 | class QuickAccessModel(QAbstractListModel): 473 | def __init__(self): 474 | QAbstractListModel.__init__(self) 475 | self.shortcuts = [] 476 | 477 | def rowCount(self, parent): 478 | if not parent.isValid(): 479 | return len(self.shortcuts) 480 | return 0 481 | 482 | def data(self, index, role): 483 | if index.isValid() and role == Qt.DisplayRole and index.column() == 0: 484 | return self.shortcuts[index.row()]["name"] 485 | elif index.isValid() and role == Qt.DecorationRole and index.column() == 0: 486 | return self.shortcuts[index.row()]["icon"] 487 | return None 488 | 489 | def addShortcut(self, name, icon, callback): 490 | self.beginInsertRows(QModelIndex(), len(self.shortcuts), len(self.shortcuts)) 491 | self.shortcuts.append({"name":name, "icon":icon, "callback":callback}) 492 | self.endInsertRows() 493 | 494 | def removeShortcut(self, name): 495 | for i, shortcut in enumerate(self.shortcuts): 496 | if shortcut["name"] != name: 497 | continue 498 | self.beginRemoveRows(QModelIndex(), i, i) 499 | self.shortcuts.pop(i) 500 | self.endRemoveRows() 501 | return 502 | 503 | def runShortcut(self, index): 504 | if not index.isValid(): 505 | return 506 | self.shortcuts[index.row()]["callback"]() 507 | 508 | 509 | class ShutterWidget(QWidget): 510 | def __init__(self, parent): 511 | QWidget.__init__(self, parent) 512 | self.setGeometry(parent.rect()) 513 | self.value = 0 514 | self.timer = QTimer() 515 | self.timer.timeout.connect(self.playNextFrame) 516 | self.timer.start(100) 517 | 518 | def playNextFrame(self): 519 | self.value += 0.05 520 | if self.value >= 0.2: 521 | self.timer.stop() 522 | self.update() 523 | 524 | def paintEvent(self, event): 525 | QWidget.paintEvent(self, event) 526 | painter = QPainter(self) 527 | painter.setBrush(QBrush(QColor(Qt.black))) 528 | painter.setPen(QPen()) 529 | painter.setOpacity(self.value) 530 | painter.drawRect(self.rect()) 531 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/canvas.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QRect 2 | from PyQt5.QtWidgets import QFrame, QSizePolicy 3 | 4 | __all__ = ["Canvas"] 5 | 6 | class Canvas(QFrame): 7 | def __init__(self, parent): 8 | QFrame.__init__(self, parent) 9 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 10 | self.widgets = [] 11 | self.previousSize = None 12 | 13 | def showWidget(self, widget): 14 | self.widgets.append(widget) 15 | factorForWidth = self.width() / 40 16 | factorForHeight = self.height() / 30 17 | vrect = widget.rect 18 | trect = QRect(vrect.left() * factorForWidth + 3, vrect.top() * factorForHeight + 3, 19 | vrect.width() * factorForWidth - 6, vrect.height() * factorForHeight - 6) 20 | widget.widget.setGeometry(trect) 21 | widget.widget.show() 22 | 23 | def closeWidget(self, widget): 24 | i = 0 25 | for widget_ in self.widgets: 26 | if widget_ is widget: 27 | self.widgets.pop(i) 28 | break 29 | i += 1 30 | 31 | def positWidgets(self, force = False): 32 | "显示QuickPanel时重新排布一下部件。因为屏幕分辨率可能会有改动什么的。" 33 | if not force and self.previousSize == self.size(): 34 | return 35 | factorForWidth = self.width() / 40 36 | factorForHeight = self.height() / 30 37 | for widget in self.widgets: 38 | vrect = widget.rect 39 | trect = QRect(vrect.left() * factorForWidth + 3, vrect.top() * factorForHeight + 3, 40 | vrect.width() * factorForWidth - 6, vrect.height() * factorForHeight - 6) 41 | widget.widget.setGeometry(trect) 42 | self.previousSize = self.size() 43 | 44 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/layout_editor.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import functools 3 | from PyQt5.QtCore import QAbstractListModel, QModelIndex, QPoint, QRect, Qt, pyqtSignal 4 | from PyQt5.QtGui import QBrush, QColor, QIcon, QPainter, QPen 5 | from PyQt5.QtWidgets import QDialog, QFrame, QMenu, QSizePolicy, QWidget 6 | from .Ui_selectwidgets import Ui_SelectWidgetsDialog 7 | 8 | __all__ = ["LayoutEditor"] 9 | 10 | class LayoutEditor(QFrame): 11 | """显示一个类似于KDE桌面的编辑界面,可以让用户添加、删除、移动部件。还可以改变部件的大小。""" 12 | 13 | def __init__(self, parent): 14 | QFrame.__init__(self, parent) 15 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 16 | 17 | def paintEvent(self, event): 18 | QFrame.paintEvent(self, event) 19 | factorForWidth = self.width() / 40 20 | factorForHeight = self.height() / 30 21 | row = 1 22 | column = 1 23 | 24 | painter = QPainter(self) 25 | painter.setOpacity(0.9) 26 | pen = QPen() 27 | pen.setColor(Qt.white) 28 | painter.setPen(pen) 29 | while factorForWidth * column < self.width(): 30 | painter.drawLine(factorForWidth * column, 0, factorForWidth * column, self.height()) 31 | column += 1 32 | while factorForHeight * row < self.height(): 33 | painter.drawLine(0, factorForHeight * row, self.width(), factorForHeight * row) 34 | row += 1 35 | 36 | def beginEdit(self, widgets): 37 | self.widgets = [] 38 | for widget in widgets: 39 | w = {} 40 | w["id"] = widget.id 41 | w["name"] = widget.name 42 | w["description"] = widget.description 43 | w["enabled"] = widget.enabled 44 | w["widget"] = None 45 | w["rect"] = widget.rect 46 | self.widgets.append(w) 47 | self.originalWidgets = copy.deepcopy(self.widgets) 48 | self.posistWidgets() 49 | 50 | def saveLayout(self, widgets): 51 | changedWidgets = [] 52 | for widget in widgets: 53 | for w in self.widgets: 54 | if w["id"] == widget.id: 55 | break 56 | else: 57 | continue 58 | changed = False 59 | if w["enabled"] != widget.enabled: 60 | widget.enabled = w["enabled"] 61 | changed = True 62 | if w["rect"] != widget.rect: 63 | widget.rect = w["rect"] 64 | changed = True 65 | if changed: 66 | changedWidgets.append(widget) 67 | return changedWidgets 68 | 69 | def endEdit(self): 70 | for widget in self.widgets: 71 | if widget["widget"] is not None: 72 | widget["widget"].close() 73 | widget["widget"].setParent(None) 74 | widget["widget"] = None 75 | 76 | def posistWidgets(self): 77 | factorForWidth = self.width() / 40 78 | factorForHeight = self.height() / 30 79 | for widget in self.widgets: 80 | if widget["enabled"]: 81 | if widget["widget"] is None: 82 | widget["widget"] = Widget(self) 83 | widget["widget"].geometryChanged.connect(self.onWidgetGeometryChanged) 84 | widget["widget"].deleteMe.connect(self.deleteWidget) 85 | widget["widget"].show() 86 | widget["widget"].setName(widget["name"]) 87 | widget["widget"].setDescription(widget["description"]) 88 | vrect = widget["rect"] 89 | trect = QRect(vrect.left() * factorForWidth + 3, vrect.top() * factorForHeight + 3, 90 | vrect.width() * factorForWidth - 6, vrect.height() * factorForHeight - 6) 91 | widget["widget"].setGeometry(trect) 92 | else: 93 | if widget["widget"] is not None: 94 | widget["widget"].setParent(None) 95 | widget["widget"] = None 96 | 97 | def resetLayout(self): 98 | self.endEdit() 99 | self.widgets = copy.deepcopy(self.originalWidgets) 100 | self.posistWidgets() 101 | 102 | def selectWidgets(self): 103 | d = SelectWidgetsDialog(self) 104 | callback = functools.partial(d.selectWidgets, self.widgets) 105 | if self.parent()._runDialog(d, self, callback) == QDialog.Accepted: 106 | result = d.getResult() 107 | for widget in self.widgets: 108 | for row in result: 109 | if widget["id"] == row["id"]: 110 | widget["enabled"] = row["enabled"] 111 | break 112 | self.posistWidgets() 113 | d.deleteLater() 114 | 115 | def onWidgetGeometryChanged(self, newRect): 116 | w = self.sender() 117 | for widget in self.widgets: 118 | if widget["widget"] is w: 119 | widget["rect"] = newRect 120 | return 121 | 122 | def deleteWidget(self): 123 | w = self.sender() 124 | for widget in self.widgets: 125 | if widget["widget"] is w: 126 | self.parent().disableWidget(widget["id"]) 127 | widget["widget"].setParent(None) 128 | widget["widget"] = None 129 | widget["enabled"] = False 130 | return 131 | 132 | 133 | class Widget(QWidget): 134 | geometryChanged = pyqtSignal("QRect") 135 | deleteMe = pyqtSignal() 136 | 137 | def __init__(self, parent): 138 | QWidget.__init__(self, parent) 139 | self.name = self.description = "" 140 | self.setMouseTracking(True) 141 | self.moving = False 142 | self.edge = 8 143 | 144 | def paintEvent(self, event): 145 | QWidget.paintEvent(self, event) 146 | pen = QPen() 147 | pen.setWidth(2) 148 | pen.setColor(QColor(200, 200, 200)) 149 | rect = self.rect() 150 | rect = rect.adjusted(0, 0, -1, -1) 151 | painter = QPainter(self) 152 | painter.setRenderHint(QPainter.Antialiasing, True) 153 | painter.setPen(pen) 154 | painter.setOpacity(0.5) 155 | painter.setBrush(QBrush(QColor(Qt.white))) 156 | painter.drawRoundedRect(rect, 15, 15) 157 | painter.setOpacity(1) 158 | pen.setColor(Qt.black) 159 | painter.setPen(pen) 160 | text = self.name + "\n" + self.description 161 | painter.drawText(self.rect(), Qt.AlignHCenter | Qt.AlignVCenter, text) 162 | 163 | def mouseMoveEvent(self, event): 164 | width = self.width() 165 | height = self.height() 166 | if self.moving: 167 | oldRect = self.geometry() 168 | oldRect.adjust( - 3, -3, 3, 3) 169 | p = event.pos() 170 | p = self.mapToParent(p) 171 | if self.orientation == "leftTop": 172 | p = self.tryMove(oldRect.topLeft(), p, 3) 173 | oldRect.setTopLeft(p) 174 | elif self.orientation == "leftBottom": 175 | p = self.tryMove(oldRect.bottomLeft(), p, 3) 176 | oldRect.setBottomLeft(p) 177 | elif self.orientation == "left": 178 | p = self.tryMove(oldRect.topLeft(), p, 1) 179 | oldRect.setLeft(p.x()) 180 | elif self.orientation == "rightTop": 181 | p = self.tryMove(oldRect.topRight(), p, 3) 182 | oldRect.setTopRight(p) 183 | elif self.orientation == "rightBottom": 184 | p = self.tryMove(oldRect.bottomRight(), p, 3) 185 | oldRect.setBottomRight(p) 186 | elif self.orientation == "right": 187 | p = self.tryMove(oldRect.topRight(), p, 1) 188 | oldRect.setRight(p.x()) 189 | elif self.orientation == "top": 190 | p = self.tryMove(oldRect.topRight(), p, 2) 191 | oldRect.setTop(p.y()) 192 | elif self.orientation == "bottom": 193 | p = self.tryMove(oldRect.bottomRight(), p, 2) 194 | oldRect.setBottom(p.y()) 195 | else: 196 | p = event.globalPos() - self.originalPos + self.originalTopLeft 197 | p = self.tryMove(self.originalTopLeft, p, 3) 198 | if oldRect.topLeft() != p: 199 | oldRect.moveTopLeft(p) 200 | #self.originalPos=event.globalPos() 201 | self.geometryChanged.emit(self.calculateLogicalRect(oldRect)) 202 | oldRect.adjust(3, 3, -3, -3) 203 | self.setGeometry(oldRect) 204 | else: 205 | if 0 < event.x() < self.edge: 206 | if 0 < event.y() < self.edge: 207 | self.setCursor(Qt.SizeFDiagCursor) 208 | elif height - self.edge < event.y() < height: 209 | self.setCursor(Qt.SizeBDiagCursor) 210 | else: 211 | self.setCursor(Qt.SizeHorCursor) 212 | elif width - self.edge < event.x() < width: 213 | if 0 < event.y() < self.edge: 214 | self.setCursor(Qt.SizeBDiagCursor) 215 | elif height - self.edge < event.y() < height: 216 | self.setCursor(Qt.SizeFDiagCursor) 217 | else: 218 | self.setCursor(Qt.SizeHorCursor) 219 | elif 0 < event.y() < self.edge or height - self.edge < event.y() < height: 220 | self.setCursor(Qt.SizeVerCursor) 221 | else: 222 | self.setCursor(Qt.SizeAllCursor) 223 | 224 | def mousePressEvent(self, event): 225 | self.raise_() 226 | if event.button() == Qt.LeftButton: 227 | self.moving = True 228 | width = self.width() 229 | height = self.height() 230 | if 0 < event.x() < self.edge: 231 | if 0 < event.y() < self.edge: 232 | self.orientation = "leftTop" 233 | elif height - self.edge < event.y() < height: 234 | self.orientation = "leftBottom" 235 | else: 236 | self.orientation = "left" 237 | elif width - self.edge < event.x() < width: 238 | if 0 < event.y() < self.edge: 239 | self.orientation = "rightTop" 240 | elif height - self.edge < event.y() < height: 241 | self.orientation = "rightBottom" 242 | else: 243 | self.orientation = "right" 244 | elif 0 < event.y() < self.edge: 245 | self.orientation = "top" 246 | elif height - self.edge < event.y() < height: 247 | self.orientation = "bottom" 248 | else: 249 | self.orientation = "center" 250 | self.originalPos = event.globalPos() 251 | self.originalTopLeft = self.geometry().topLeft() 252 | self.originalTopLeft += QPoint( - 3, -3) 253 | elif event.button() == Qt.RightButton: 254 | menu = QMenu() 255 | actionDelete = menu.addAction(QIcon(":/images/remove.png"), self.tr("删除(&R)")) 256 | try: 257 | result = getattr(menu, "exec_")(event.globalPos()) 258 | except AttributeError: 259 | result = getattr(menu, "exec")(event.globalPos()) 260 | if result is actionDelete: 261 | self.deleteMe.emit() 262 | 263 | def mouseReleaseEvent(self, event): 264 | self.moving = False 265 | try: 266 | del self.originalPos 267 | del self.originalTopLeft 268 | except AttributeError: 269 | pass 270 | 271 | def setName(self, name): 272 | self.name = name 273 | 274 | def setDescription(self, description): 275 | self.description = description 276 | 277 | def tryMove(self, oldPos, newPos, directions): 278 | p = QPoint(oldPos) 279 | if directions & 1: #X轴方向 280 | gridX = self.parent().width() / 40 281 | delta = newPos.x() - oldPos.x() 282 | if abs(delta) / gridX > 0.5: 283 | newX = oldPos.x() + delta / abs(delta) * gridX * round(abs(delta) / gridX) 284 | newX = gridX * round(newX / gridX) 285 | p.setX(newX) 286 | if directions & 2: 287 | gridY = self.parent().height() / 30 288 | delta = newPos.y() - oldPos.y() 289 | if abs(delta) / gridY > 0.5: 290 | newY = oldPos.y() + delta / abs(delta) * gridY * round(abs(delta) / gridY) 291 | newY = gridY * round(newY / gridY) 292 | p.setY(newY) 293 | return p 294 | 295 | def calculateLogicalRect(self, physicalRect): 296 | gridX = self.parent().width() / 40 297 | gridY = self.parent().height() / 30 298 | logicalRect = QRect() 299 | logicalRect.setTop(round(physicalRect.top() / gridY)) 300 | logicalRect.setLeft(round(physicalRect.left() / gridX)) 301 | logicalRect.setWidth(round(physicalRect.width() / gridX)) 302 | logicalRect.setHeight(round(physicalRect.height() / gridY)) 303 | return logicalRect 304 | 305 | class SelectWidgetsDialog(QDialog, Ui_SelectWidgetsDialog): 306 | def __init__(self, parent): 307 | QDialog.__init__(self, parent) 308 | self.setupUi(self) 309 | self.widgetsModel = WidgetsModel() 310 | self.lstWidgets.setModel(self.widgetsModel) 311 | self.lstWidgets.selectionModel().currentChanged.connect(self.onCurrentWidgetChanged) 312 | 313 | def selectWidgets(self, widgets): 314 | self.widgetsModel.setWidgets(widgets) 315 | self.lstWidgets.setCurrentIndex(self.widgetsModel.firstIndex()) 316 | try: 317 | return getattr(self, "exec_")() 318 | except AttributeError: 319 | return getattr(self, "exec")() 320 | 321 | def getResult(self): 322 | return self.widgetsModel.getResult() 323 | 324 | def onCurrentWidgetChanged(self, current, previous): 325 | description = self.widgetsModel.descriptionFor(current) 326 | self.lblDescription.setText(description) 327 | 328 | 329 | class WidgetsModel(QAbstractListModel): 330 | def __init__(self): 331 | QAbstractListModel.__init__(self) 332 | self.widgets = [] 333 | 334 | def setWidgets(self, widgets): 335 | self.beginResetModel() 336 | self.widgets = [] 337 | for widget in widgets: 338 | w = {} 339 | w["id"] = widget["id"] 340 | w["name"] = widget["name"] 341 | w["description"] = widget["description"] 342 | w["enabled"] = widget["enabled"] 343 | self.widgets.append(w) 344 | self.endResetModel() 345 | 346 | def rowCount(self, parent): 347 | if parent.isValid(): 348 | return 0 349 | return len(self.widgets) 350 | 351 | def data(self, index, role): 352 | if index.isValid(): 353 | widget = self.widgets[index.row()] 354 | if role == Qt.DisplayRole: 355 | return widget["name"] 356 | elif role == Qt.CheckStateRole: 357 | return Qt.Checked if widget["enabled"] else Qt.Unchecked 358 | return None 359 | 360 | def setData(self, index, value, role): 361 | if index.isValid() and role == Qt.CheckStateRole: 362 | widget = self.widgets[index.row()] 363 | state = value 364 | widget["enabled"] = (state == Qt.Checked) 365 | self.dataChanged.emit(index, index) 366 | return True 367 | return False 368 | 369 | def flags(self, index): 370 | if index.isValid(): 371 | return QAbstractListModel.flags(self, index) | Qt.ItemIsUserCheckable 372 | return QAbstractListModel.flags(self, index) 373 | 374 | def getResult(self): 375 | return self.widgets 376 | 377 | def descriptionFor(self, index): 378 | if index.isValid(): 379 | widget = self.widgets[index.row()] 380 | return widget["description"] 381 | return "" 382 | 383 | def firstIndex(self): 384 | if len(self.widgets) == 0: 385 | return QModelIndex() 386 | return self.createIndex(0, 0) 387 | 388 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/selectwidgets.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SelectWidgetsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | 选择快捷面板部件 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | buttonBox 50 | accepted() 51 | SelectWidgetsDialog 52 | accept() 53 | 54 | 55 | 199 56 | 278 57 | 58 | 59 | 199 60 | 149 61 | 62 | 63 | 64 | 65 | buttonBox 66 | rejected() 67 | SelectWidgetsDialog 68 | reject() 69 | 70 | 71 | 199 72 | 278 73 | 74 | 75 | 199 76 | 149 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/services.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from besteam.utils.sql import Database, Table 3 | 4 | __all__ = ["WidgetConfigure", "QuickPanelDatabase"] 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class WidgetConfigure: 9 | id = None 10 | name = None 11 | description = None 12 | factory = None 13 | rect = None 14 | enabled = False 15 | widget = None 16 | 17 | def __repr__(self): 18 | return repr(self.__dict__) 19 | 20 | 21 | class QuickPanelWidget(Table): 22 | columns = { 23 | "id":"text", 24 | "enabled":"bool", #当前是否启用 25 | "left":"number", #接下来的left, top, width, height纪录插件的位置,如果插件没有被启用。这四个字段是没有意义的 26 | "top":"number", 27 | "width":"number", 28 | "height":"number", 29 | } 30 | 31 | class QuickPanelDatabase(Database): 32 | tables = (QuickPanelWidget, ) 33 | 34 | def createInitialData(self, table): 35 | if table is QuickPanelWidget: 36 | #init from left to right, and then from top to bottom 37 | 38 | desktopIconWidget = {} 39 | desktopIconWidget["id"] = "dd6afcb0-e223-4156-988d-20f20266c6f0" 40 | desktopIconWidget["enabled"] = True 41 | desktopIconWidget["left"] = 0 42 | desktopIconWidget["top"] = 0 43 | desktopIconWidget["width"] = 20 44 | desktopIconWidget["height"] = 22 45 | self.insertQuickPanelWidget(desktopIconWidget) 46 | 47 | machineLoadWidget = {} 48 | machineLoadWidget["id"] = "b0b6b9eb-aec0-4fe5-bfd0-d4d317fdd547" 49 | machineLoadWidget["enabled"] = True 50 | machineLoadWidget["left"] = 0 51 | machineLoadWidget["top"] = 22 52 | machineLoadWidget["width"] = 20 53 | machineLoadWidget["height"] = 5 54 | self.insertQuickPanelWidget(machineLoadWidget) 55 | 56 | calendarWidget = {} 57 | calendarWidget["id"] = "d94db588-663b-4a6f-b935-4ca9ff283c75" 58 | calendarWidget["enabled"] = True 59 | calendarWidget["left"] = 0 60 | calendarWidget["top"] = 27 61 | calendarWidget["width"] = 20 62 | calendarWidget["height"] = 3 63 | self.insertQuickPanelWidget(calendarWidget) 64 | 65 | quickAccessWidget = {} 66 | quickAccessWidget["id"] = "be6c197b-0181-47c0-a9fc-6a1fe5f1b3e6" 67 | quickAccessWidget["enabled"] = True 68 | quickAccessWidget["left"] = 20 69 | quickAccessWidget["top"] = 0 70 | quickAccessWidget["width"] = 20 71 | quickAccessWidget["height"] = 6 72 | quickAccessWidget["factory"] = "im.quick_panel.widgets.quick_access.QuickAccessWidget" 73 | self.insertQuickPanelWidget(quickAccessWidget) 74 | 75 | todoListWidget = {} 76 | todoListWidget["id"] = "bc8ada4f-50b8-49f7-917a-da163b6763e9" 77 | todoListWidget["enabled"] = True 78 | todoListWidget["left"] = 20 79 | todoListWidget["top"] = 6 80 | todoListWidget["width"] = 20 81 | todoListWidget["height"] = 16 82 | self.insertQuickPanelWidget(todoListWidget) 83 | 84 | textpadWidget = {} 85 | textpadWidget["id"] = "45d1ee54-f9bd-435e-93cf-b46a05b56514" 86 | textpadWidget["enabled"] = True 87 | textpadWidget["left"] = 20 88 | textpadWidget["top"] = 22 89 | textpadWidget["width"] = 20 90 | textpadWidget["height"] = 8 91 | self.insertQuickPanelWidget(textpadWidget) 92 | 93 | def getWidgetConfig(self, id): 94 | rows = self.selectQuickPanelWidget("where id=?", id) 95 | if not rows: 96 | return None 97 | return dict(rows[0]) 98 | 99 | def saveWidgetConfig(self, config): 100 | rows = self.selectQuickPanelWidget("where id=?", config["id"]) 101 | if rows: 102 | self.updateQuickPanelWidget(config, "where id=?", config["id"]) 103 | else: 104 | self.insertQuickPanelWidget(config) 105 | 106 | def setWidgetEnabled(self, id, enabled): 107 | self.updateQuickPanelWidget({"enabled": enabled}, "where id=?", id) 108 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/bookmark.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | BookmarkDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 361 10 | 150 11 | 12 | 13 | 14 | 网络链接 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 名称: 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 网址: 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Qt::Vertical 45 | 46 | 47 | 48 | 20 49 | 40 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | buttonBox 67 | accepted() 68 | BookmarkDialog 69 | accept() 70 | 71 | 72 | 180 73 | 128 74 | 75 | 76 | 180 77 | 74 78 | 79 | 80 | 81 | 82 | buttonBox 83 | rejected() 84 | BookmarkDialog 85 | reject() 86 | 87 | 88 | 180 89 | 128 90 | 91 | 92 | 180 93 | 74 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/calendar.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QDateTime, Qt, QRectF, QPointF, QSizeF, QTimer 2 | from PyQt5.QtGui import QFontMetrics 3 | from PyQt5.QtWidgets import QFrame, QStylePainter 4 | 5 | class CalendarWidget(QFrame): 6 | def __init__(self, parent = None): 7 | QFrame.__init__(self, parent) 8 | self.timer = QTimer() 9 | self.timer.setInterval(1000) 10 | self.timer.timeout.connect(self.updateCurrentDateTime) 11 | self.timer.start() 12 | 13 | def paintEvent(self, event): 14 | QFrame.paintEvent(self, event) 15 | text = QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) 16 | logicalRect = QRectF(QPointF(0, 0), QSizeF(QFontMetrics(self.font()).size(Qt.TextSingleLine, text))) 17 | physicalRect, frameWidth = QRectF(self.rect()), self.frameWidth() 18 | physicalRect.adjust(frameWidth, frameWidth, -frameWidth, -frameWidth) 19 | scaleForWidth = physicalRect.width() / logicalRect.width() 20 | scaleForHeight = physicalRect.height() / logicalRect.height() 21 | logicalRect.moveTo(frameWidth / scaleForWidth , frameWidth / scaleForHeight) 22 | 23 | painter = QStylePainter(self) 24 | painter.scale(scaleForWidth, scaleForHeight) 25 | painter.drawText(logicalRect, Qt.AlignCenter, text) 26 | 27 | def updateCurrentDateTime(self): 28 | if self.isVisible(): 29 | self.update() 30 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/desktop_icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import uuid 4 | from PyQt5.QtCore import QAbstractListModel, QFileInfo, QModelIndex, QProcess, QSize, QUrl, Qt, \ 5 | QStandardPaths 6 | from PyQt5.QtGui import QDesktopServices, QIcon, QImage, QPixmap, QCursor 7 | from PyQt5.QtWidgets import QAbstractItemView, QListView, QMenu, QHBoxLayout, QFileDialog, \ 8 | QMessageBox, QDialog, QFrame, QFileIconProvider, QAction 9 | from besteam.utils.sql import Table, Database 10 | from .Ui_shortcut import Ui_ShortcutDialog 11 | from .Ui_bookmark import Ui_BookmarkDialog 12 | 13 | __all__ = ["DesktopIconWidget"] 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | #定义几个特殊路径,分别是我的电脑,我的文档,我的音乐,我的图片 18 | COMPUTER_PATH = "special://29307059-59dc-47e6-8c43-d77db6489d3a" 19 | DOCUMENTS_PATH = "special://688910d6-de73-4426-b8a1-75dd03b91e3e" 20 | MUSIC_PATH = "special://f1fd7171-fb82-47c4-9032-f7af3032e75e" 21 | PICTURES_PATH = "special://a8350c8f-27db-4b82-add1-613351da2bd4" 22 | 23 | class DesktopIconWidget(QFrame): 24 | def __init__(self, parent): 25 | QFrame.__init__(self, parent) 26 | self.setFrameStyle(QFrame.Box | QFrame.Sunken) 27 | self.setStyleSheet("QListView{background:transparent;}") 28 | 29 | self.listView = QListView(self) 30 | self.setLayout(QHBoxLayout()) 31 | self.layout().setContentsMargins(0, 0, 0, 0) 32 | self.layout().addWidget(self.listView) 33 | 34 | self.listView.setContextMenuPolicy(Qt.CustomContextMenu) 35 | self.listView.setEditTriggers(QAbstractItemView.NoEditTriggers) 36 | self.listView.setMovement(QListView.Snap) 37 | self.listView.setFlow(QListView.LeftToRight) 38 | self.listView.setResizeMode(QListView.Adjust) 39 | self.listView.setGridSize(QSize(self.logicalDpiX() / 96 * 70, 40 | self.logicalDpiY() / 96 * 70)) 41 | self.listView.setViewMode(QListView.IconMode) 42 | 43 | self.quickDesktopModel = QuickDesktopModel(self.window().platform.databaseFile) 44 | self.listView.setModel(self.quickDesktopModel) 45 | self.createActions() 46 | self.makeConnections() 47 | 48 | def createActions(self): 49 | self.actionCreateComputer = QAction(self.tr("我的电脑(&C)"), self) 50 | self.actionCreateDocuments = QAction(self.tr("我的文档(&D)"), self) 51 | self.actionCreateMusic = QAction(self.tr("我的音乐(&M)"), self) 52 | self.actionCreatePictures = QAction(self.tr("我的图片(&P)"), self) 53 | self.actionCreateShortcut = QAction(self.tr("创建快捷方式(&C)"), self) 54 | self.actionCreateShortcut.setIcon(QIcon(":/images/new.png")) 55 | self.actionCreateBookmark = QAction(self.tr("创建网络链接(&B)"), self) 56 | self.actionCreateBookmark.setIcon(QIcon(":/images/insert-link.png")) 57 | self.actionRemoveShortcut = QAction(self.tr("删除快捷方式(&R)"), self) 58 | self.actionRemoveShortcut.setIcon(QIcon(":/images/delete.png")) 59 | self.actionRenameShortcut = QAction(self.tr("重命名(&N)"), self) 60 | self.actionRenameShortcut.setIcon(QIcon(":/images/edit-rename.png")) 61 | self.actionEditShortcut = QAction(self.tr("编辑快捷方式(&E)"), self) 62 | self.actionEditShortcut.setIcon(QIcon(":/images/edit.png")) 63 | 64 | def makeConnections(self): 65 | self.listView.customContextMenuRequested.connect(self.onQuickDesktopContextMenuRequest) 66 | self.listView.activated.connect(self.runQuickDesktopShortcut) 67 | 68 | self.actionCreateComputer.triggered.connect(self.createComputerShortcut) 69 | self.actionCreateDocuments.triggered.connect(self.createDocumentsShortcut) 70 | self.actionCreateMusic.triggered.connect(self.createMusicShortcut) 71 | self.actionCreatePictures.triggered.connect(self.createPicturesShortcut) 72 | self.actionCreateShortcut.triggered.connect(self.createShortcut) 73 | self.actionCreateBookmark.triggered.connect(self.createBookmark) 74 | self.actionEditShortcut.triggered.connect(self.editShortcut) 75 | self.actionRemoveShortcut.triggered.connect(self.removeShortcut) 76 | self.actionRenameShortcut.triggered.connect(self.renameShortcut) 77 | 78 | def onQuickDesktopContextMenuRequest(self, pos): 79 | index = self.listView.indexAt(pos) 80 | self.listView.setCurrentIndex(index) 81 | menu = QMenu() 82 | menu.addAction(self.actionCreateShortcut) 83 | menu.addAction(self.actionCreateBookmark) 84 | menu2 = menu.addMenu(self.tr("创建特殊快捷方式(&S)")) 85 | if os.name == "nt": 86 | menu2.addAction(self.actionCreateComputer) 87 | menu2.addAction(self.actionCreateDocuments) 88 | menu2.addAction(self.actionCreatePictures) 89 | menu2.addAction(self.actionCreateMusic) 90 | if index.isValid(): 91 | menu.addAction(self.actionRemoveShortcut) 92 | if not self.quickDesktopModel.isSpecialShortcut(index): 93 | menu.addAction(self.actionEditShortcut) 94 | menu.addAction(self.actionRenameShortcut) 95 | try: 96 | getattr(menu, "exec")(QCursor.pos()) 97 | except AttributeError: 98 | getattr(menu, "exec_")(QCursor.pos()) 99 | 100 | def createShortcut(self): 101 | d = ShortcutDialog(self) 102 | if self.window().runDialog(d.create) == QDialog.Accepted: 103 | shortcut = d.getResult() 104 | shortcut["id"] = str(uuid.uuid4()) 105 | self.quickDesktopModel.addShortcut(shortcut) 106 | d.deleteLater() 107 | 108 | def createBookmark(self): 109 | d = BookmarkDialog(self) 110 | if self.window().runDialog(d.create) == QDialog.Accepted: 111 | shortcut = { 112 | "id": str(uuid.uuid4()), 113 | "icon": "", 114 | "openwith": "", 115 | "dir": "", 116 | } 117 | shortcut.update(d.getResult()) 118 | self.quickDesktopModel.addShortcut(shortcut) 119 | d.deleteLater() 120 | 121 | def createComputerShortcut(self): 122 | shortcut = { 123 | "id": str(uuid.uuid4()), 124 | "name": self.tr("我的电脑"), 125 | "path": COMPUTER_PATH, 126 | "icon": "", 127 | "dir": "", 128 | "openwith": "", 129 | } 130 | self.quickDesktopModel.addShortcut(shortcut) 131 | 132 | def createDocumentsShortcut(self): 133 | shortcut = { 134 | "id": str(uuid.uuid4()), 135 | "name": self.tr("我的文档"), 136 | "path": DOCUMENTS_PATH, 137 | "icon": "", 138 | "dir": "", 139 | "openwith": "", 140 | } 141 | self.quickDesktopModel.addShortcut(shortcut) 142 | 143 | def createPicturesShortcut(self): 144 | shortcut = { 145 | "id": str(uuid.uuid4()), 146 | "name": self.tr("图片收藏"), 147 | "path": PICTURES_PATH, 148 | "icon": "", 149 | "dir": "", 150 | "openwith": "", 151 | } 152 | self.quickDesktopModel.addShortcut(shortcut) 153 | 154 | def createMusicShortcut(self): 155 | shortcut = { 156 | "id": str(uuid.uuid4()), 157 | "name": self.tr("我的音乐"), 158 | "path": MUSIC_PATH, 159 | "icon": "", 160 | "dir": "", 161 | "openwith": "", 162 | } 163 | self.quickDesktopModel.addShortcut(shortcut) 164 | 165 | def renameShortcut(self): 166 | self.listView.edit(self.listView.currentIndex()) 167 | 168 | def removeShortcut(self): 169 | self.quickDesktopModel.removeShortcut(self.listView.currentIndex()) 170 | 171 | def editShortcut(self): 172 | index = self.listView.currentIndex() 173 | if not index.isValid(): 174 | return 175 | shortcut = self.quickDesktopModel.shortcutAt(index) 176 | url = QUrl.fromUserInput(shortcut["path"]) 177 | if not url.isValid(): 178 | return 179 | if url.scheme() == "special": 180 | QMessageBox.information(self, self.tr("编辑快捷方式"), self.tr("不能编辑特殊图标。")) 181 | return 182 | elif url.scheme() == "file": 183 | d = ShortcutDialog(self) 184 | else: 185 | d = BookmarkDialog(self) 186 | if self.window().runDialog(d.edit, shortcut) == QDialog.Accepted: 187 | shortcut.update(d.getResult()) 188 | self.quickDesktopModel.updateShortcut(shortcut, index) 189 | d.deleteLater() 190 | 191 | def runQuickDesktopShortcut(self): 192 | index = self.listView.currentIndex() 193 | if not index.isValid(): 194 | return 195 | if not self.quickDesktopModel.runShortcut(index): 196 | QMessageBox.information(self, self.tr("快捷面板"), \ 197 | self.tr("不能运行快捷方式。请检查文件是否存在或者程序是否正确。")) 198 | else: 199 | self.window().close() 200 | 201 | def getShortcutIcon(shortcut): 202 | if shortcut["icon"]: 203 | icon = QIcon(shortcut["icon"]) 204 | if not icon.isNull(): 205 | return icon 206 | iconProvider = QFileIconProvider() 207 | if shortcut["path"] == COMPUTER_PATH: 208 | return QIcon(":/images/user-home.png") 209 | elif shortcut["path"] == DOCUMENTS_PATH: 210 | documentsIcon = iconProvider.icon(QFileInfo(QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation))) 211 | if documentsIcon.isNull(): 212 | return QIcon(":/images/folder-documents.png") 213 | else: 214 | return documentsIcon 215 | elif shortcut["path"] == MUSIC_PATH: 216 | musicIcon = iconProvider.icon(QFileInfo(QStandardPaths.writableLocation(QStandardPaths.MusicLocation))) 217 | if musicIcon.isNull(): 218 | return QIcon(":/images/folder-sound.png") 219 | else: 220 | return musicIcon 221 | elif shortcut["path"] == PICTURES_PATH: 222 | picturesIcon = iconProvider.icon(QFileInfo(QStandardPaths.writableLocation(QStandardPaths.PicturesLocation))) 223 | if picturesIcon.isNull(): 224 | return QIcon(":/images/folder-image.png") 225 | else: 226 | return picturesIcon 227 | else: 228 | url = QUrl.fromUserInput(shortcut["path"]) 229 | if url.scheme() == "file": 230 | if os.path.exists(shortcut["path"]): 231 | icon = iconProvider.icon(QFileInfo(url.toLocalFile())) 232 | if not icon.isNull(): 233 | return icon 234 | return QIcon(":/images/unknown.png") 235 | else: 236 | return QIcon(":/images/httpurl.png") 237 | return QIcon(":/images/unknown.png") 238 | 239 | class QuickDesktopModel(QAbstractListModel): 240 | def __init__(self, databaseFile): 241 | QAbstractListModel.__init__(self) 242 | self.db = ShortcutDatabase(databaseFile) 243 | self.shortcuts = self.db.selectShortcut("") 244 | 245 | def rowCount(self, parent): 246 | if not parent.isValid(): 247 | return len(self.shortcuts) 248 | return 0 249 | 250 | def data(self, index, role): 251 | if index.isValid() and index.column() == 0 and role in (Qt.DisplayRole, Qt.EditRole): 252 | return self.shortcuts[index.row()]["name"] 253 | elif index.isValid() and index.column() == 0 and role == Qt.DecorationRole: 254 | shortcut = self.shortcuts[index.row()] 255 | if "_icon" not in shortcut: 256 | shortcut["_icon"] = getShortcutIcon(shortcut) 257 | return shortcut["_icon"] 258 | return None 259 | 260 | def setData(self, index, value, role): 261 | if not index.isValid() or role != Qt.EditRole: 262 | return False 263 | shortcut = self.shortcuts[index.row()] 264 | shortcut["name"] = value 265 | shortcut.save() 266 | self.dataChanged.emit(index, index) 267 | return True 268 | 269 | def flags(self, index): 270 | if index.column() == 0: 271 | return QAbstractListModel.flags(self, index) | Qt.ItemIsEditable 272 | return QAbstractListModel.flags(self, index) 273 | 274 | def addShortcut(self, shortcut): 275 | self.beginInsertRows(QModelIndex(), len(self.shortcuts), len(self.shortcuts)) 276 | shortcut = self.db.insertShortcut(shortcut) 277 | self.shortcuts.append(shortcut) 278 | self.endInsertRows() 279 | 280 | def removeShortcut(self, index): 281 | if not index.isValid(): 282 | return 283 | self.beginRemoveRows(QModelIndex(), index.row(), index.row()) 284 | shortcut = self.shortcuts.pop(index.row()) 285 | shortcut.deleteFromDatabase() 286 | self.endRemoveRows() 287 | 288 | def isSpecialShortcut(self, index): 289 | if not index.isValid(): 290 | return False 291 | return self.shortcuts[index.row()]["path"].startswith("special://") 292 | 293 | def runShortcut(self, index): 294 | if not index.isValid(): 295 | return False 296 | shortcut = self.shortcuts[index.row()] 297 | if shortcut["path"].startswith("special://"): 298 | if shortcut["path"] == COMPUTER_PATH: 299 | if os.name == "nt": 300 | explorer = os.path.join(os.environ["SystemRoot"], "explorer.exe") 301 | return QProcess.startDetached(explorer, ["::{20D04FE0-3AEA-1069-A2D8-08002B30309D}"]) 302 | else: 303 | path = "/" 304 | elif shortcut["path"] == DOCUMENTS_PATH: 305 | path = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) 306 | elif shortcut["path"] == MUSIC_PATH: 307 | path = QStandardPaths.writableLocation(QStandardPaths.MusicLocation) 308 | elif shortcut["path"] == PICTURES_PATH: 309 | path = QStandardPaths.writableLocation(QStandardPaths.PicturesLocation) 310 | else: 311 | return False 312 | if os.name == "nt": #针对windows进行优化 313 | explorer = os.path.join(os.environ["SystemRoot"], "explorer.exe") 314 | return QProcess.startDetached(explorer, [path]) 315 | else: 316 | return QDesktopServices.openUrl(QUrl.fromLocalFile(path)) 317 | else: 318 | currentDirectory = os.getcwd() 319 | try: 320 | if shortcut["dir"] is not None and shortcut["dir"] != "": 321 | os.chdir(shortcut["dir"]) 322 | if shortcut["openwith"] is not None and shortcut["openwith"] != "": 323 | if not os.path.exists(shortcut["openwith"]): 324 | return False 325 | return QProcess.startDetached(shortcut["openwith"], [shortcut["path"]]) 326 | else: 327 | url = QUrl.fromUserInput(shortcut["path"]) 328 | if not url.isValid(): 329 | return False 330 | if url.scheme() == "file" and not os.path.exists(url.toLocalFile()): 331 | return False 332 | return QDesktopServices.openUrl(url) 333 | except OSError: #raised by chdir() 334 | pass 335 | finally: 336 | os.chdir(currentDirectory) 337 | return False 338 | 339 | def shortcutAt(self, index): 340 | if index.isValid(): 341 | return self.shortcuts[index.row()] 342 | return None 343 | 344 | def updateShortcut(self, shortcut, index): 345 | self.dataChanged.emit(index, index) 346 | oldone = self.shortcuts[index.row()] 347 | for field in shortcut: 348 | oldone[field] = shortcut[field] 349 | oldone.save() 350 | del self.shortcuts[index.row()]["_icon"] 351 | 352 | 353 | class ShortcutDialog(QDialog, Ui_ShortcutDialog): 354 | "快捷方式编辑器。可以用于创建和编辑桌面快捷方式。" 355 | def __init__(self, parent): 356 | QDialog.__init__(self, parent) 357 | self.setupUi(self) 358 | self.makeConnections() 359 | 360 | def makeConnections(self): 361 | self.btnBrowsePath.clicked.connect(self.browsePath) 362 | self.btnBrowseOpenwith.clicked.connect(self.browseOpenwith) 363 | self.btnBrowseDir.clicked.connect(self.browseDir) 364 | self.btnChangeIcon.clicked.connect(self.changeFileIcon) 365 | self.btnRestoreDir.clicked.connect(self.restoreDir) 366 | self.btnRestoreIcon.clicked.connect(self.restoreIcon) 367 | self.txtPath.textEdited.connect(self.setFileIcon) 368 | 369 | def create(self): 370 | self.mode = "create" 371 | self.setWindowTitle(self.tr("创建快捷方式")) 372 | self.btnOkay.setText(self.tr("创建(&O)")) 373 | self.iconPath = "" 374 | self.shortcutIcon = QIcon(":/images/unknown.png") 375 | self.btnFace.setIcon(self.shortcutIcon) 376 | try: 377 | return getattr(self, "exec")() 378 | except AttributeError: 379 | return getattr(self, "exec_")() 380 | 381 | def showEvent(self, event): 382 | if self.mode == "create": 383 | self.browsePath() 384 | return QDialog.showEvent(self, event) 385 | 386 | def edit(self, shortcut): 387 | self.mode = "edit" 388 | self.setWindowTitle(self.tr("编辑快捷方式")) 389 | icon = getShortcutIcon(shortcut) 390 | self.btnFace.setIcon(icon) 391 | self.shortcutIcon = icon 392 | self.iconPath = shortcut["icon"] 393 | self.txtPath.setText(shortcut["path"]) 394 | self.txtName.setText(shortcut["name"]) 395 | self.txtOpenwith.setText(shortcut["openwith"]) 396 | self.txtDir.setText(shortcut["dir"]) 397 | try: 398 | return getattr(self, "exec_")() 399 | except AttributeError: 400 | return getattr(self, "exec")() 401 | 402 | def accept(self): 403 | if self.txtName.text().strip() == "": 404 | QMessageBox.information(self, self.windowTitle(), 405 | self.tr("请填写快捷方式的名称。")) 406 | self.txtName.setFocus(Qt.OtherFocusReason) 407 | return 408 | path = self.txtPath.text().strip() 409 | if path == "": 410 | QMessageBox.information(self, self.windowTitle(), 411 | self.tr("请填写目标文件/程序。")) 412 | self.txtPath.setFocus(Qt.OtherFocusReason) 413 | self.txtPath.selectAll() 414 | return 415 | if not os.path.exists(path): 416 | QMessageBox.information(self, self.windowTitle(), 417 | self.tr("目标文件/程序不存在。")) 418 | self.txtPath.setFocus(Qt.OtherFocusReason) 419 | self.txtPath.selectAll() 420 | return 421 | openwith = self.txtOpenwith.text().strip() 422 | if openwith != "": 423 | if not os.path.exists(openwith): 424 | QMessageBox.information(self, self.windowTitle(), 425 | self.tr("编辑程序不存在。请重新选择。该选项是选填项,并不一定要填写。")) 426 | self.txtOpenwith.setFocus(Qt.OtherFocusReason) 427 | self.txtOpenwith.selectAll() 428 | return 429 | fi = QFileInfo(openwith) 430 | if not fi.isExecutable(): 431 | QMessageBox.information(self, self.windowTitle(), 432 | self.tr("编辑程序必须是一个可执行文件。请重新选择。该选项是选填项,并不一定要填写。")) 433 | self.txtOpenwith.setFocus(Qt.OtherFocusReason) 434 | self.txtOpenwith.selectAll() 435 | return 436 | dir = self.txtDir.text().strip() 437 | if dir == "": 438 | QMessageBox.information(self, self.windowTitle(), 439 | self.tr("请填写运行目录。可以使用“默认运行目录”按钮恢复默认的运行目录。")) 440 | self.txtDir.setFocus(Qt.OtherFocusReason) 441 | self.txtDir.selectAll() 442 | return 443 | if not os.path.exists(dir): 444 | QMessageBox.information(self, self.windowTitle(), 445 | self.tr("运行目录不存在。请重新选择。可以使用“默认运行目录”按钮恢复默认的运行目录。")) 446 | self.txtDir.setFocus(Qt.OtherFocusReason) 447 | self.txtDir.selectAll() 448 | return 449 | if not os.path.isdir(dir): 450 | QMessageBox.information(self, self.windowTitle(), 451 | self.tr("运行目录必须是一个目录,而非文件。请重新选择。可以使用“默认运行目录”按钮恢复默认的运行目录。")) 452 | self.txtDir.setFocus(Qt.OtherFocusReason) 453 | self.txtDir.selectAll() 454 | return 455 | QDialog.accept(self) 456 | 457 | def changeFileIcon(self): 458 | "用户点击了更换图标按钮。" 459 | filename, selectedFilter = QFileDialog.getOpenFileName(self, self.windowTitle()) 460 | if not filename: 461 | return 462 | image = QImage(filename) 463 | if not image.isNull(): 464 | self.shortcutIcon = QIcon(QPixmap.fromImage(image)) 465 | else: 466 | ip = QFileIconProvider() 467 | shortcutIcon = ip.icon(QFileInfo(filename)) 468 | if shortcutIcon.isNull(): 469 | QMessageBox.information(self, self.tr("更换图标"), 470 | self.tr("您选择的文件不包含任何可以使用的图标。")) 471 | return 472 | self.shortcutIcon = shortcutIcon 473 | self.iconPath = filename 474 | self.btnFace.setIcon(self.shortcutIcon) 475 | 476 | def restoreIcon(self): 477 | self.iconPath = "" 478 | self.shortcutIcon = getShortcutIcon(self.getResult()) 479 | self.btnFace.setIcon(self.shortcutIcon) 480 | 481 | def restoreDir(self): 482 | "用户点击了“默认运行目录”按钮。" 483 | path = self.txtPath.text().strip() 484 | fi = QFileInfo(path) 485 | if path == "" or not fi.exists(): 486 | return 487 | self.txtDir.setText(fi.dir().absolutePath()) 488 | 489 | def browseDir(self): 490 | "用户点击了浏览运行目录按钮。" 491 | dirName = QFileDialog.getExistingDirectory(self, self.windowTitle(), self.txtDir.text()) 492 | if dirName == "": 493 | return 494 | self.txtDir.setText(dirName) 495 | 496 | def browsePath(self): 497 | """用户点击了浏览路径的按钮。如果成功设置了路径,就返回True,如果用户取消了操作或者出错,就返回False 498 | 返回的用途参见showEvent()""" 499 | filename, selectedFilter = QFileDialog.getOpenFileName(self, self.windowTitle()) 500 | if not filename: 501 | return False 502 | fi = QFileInfo(filename) 503 | if fi.isSymLink(): 504 | filename = fi.symLinkTarget() 505 | if not os.path.exists(filename): 506 | QMessageBox.information(self, self.windowTitle(), self.tr("快捷方式所指向的程序不正确。")) 507 | return False 508 | fi = QFileInfo(filename) 509 | self.txtName.setText(fi.baseName()) 510 | self.txtPath.setText(fi.absoluteFilePath()) 511 | self.setFileIcon(fi.absoluteFilePath()) 512 | self.txtDir.setText(fi.dir().absolutePath()) 513 | return True 514 | 515 | def setFileIcon(self, path): 516 | "每当txtPath的值改变时,就设置快捷方式的图标" 517 | fi = QFileInfo(path) 518 | if not fi.exists(): 519 | self.shortcutIcon = QIcon(":/images/unknown.png") 520 | else: 521 | ip = QFileIconProvider() 522 | self.shortcutIcon = ip.icon(fi) 523 | self.btnFace.setIcon(self.shortcutIcon) 524 | 525 | def browseOpenwith(self): 526 | filename, selectedFilter = QFileDialog.getOpenFileName(self, self.windowTitle()) 527 | if not filename: 528 | return 529 | fi = QFileInfo(filename) 530 | if fi.isSymLink(): 531 | filename = fi.symLinkTarget() 532 | if not os.path.exists(filename): 533 | QMessageBox.information(self, self.windowTitle(), 534 | self.tr("快捷方式所指向的程序不正确。")) 535 | return 536 | fi = QFileInfo(filename) 537 | if not fi.isExecutable(): 538 | QMessageBox.information(self, self.windowTitle(), 539 | self.tr("编辑程序必须是一个可执行文件。请重新选择。该选项是选填项,并不一定要填写。")) 540 | self.txtOpenwith.setText(fi.absoluteFilePath()) 541 | 542 | def getResult(self): 543 | shortcut = {} 544 | shortcut["name"] = self.txtName.text().strip() 545 | shortcut["icon"] = self.iconPath 546 | shortcut["path"] = self.txtPath.text().strip() 547 | shortcut["openwith"] = self.txtOpenwith.text().strip() 548 | shortcut["dir"] = self.txtDir.text().strip() 549 | return shortcut 550 | 551 | 552 | class BookmarkDialog(QDialog, Ui_BookmarkDialog): 553 | def __init__(self, parent): 554 | QDialog.__init__(self, parent) 555 | self.setupUi(self) 556 | self.txtLink.setFocus(Qt.OtherFocusReason) 557 | 558 | def create(self): 559 | try: 560 | return getattr(self, "exec_")() 561 | except AttributeError: 562 | return getattr(self, "exec")() 563 | 564 | def edit(self, bookmark): 565 | self.txtName.setText(bookmark["name"]) 566 | self.txtLink.setText(bookmark["path"]) 567 | try: 568 | return getattr(self, "exec_")() 569 | except AttributeError: 570 | return getattr(self, "exec")() 571 | 572 | def getResult(self): 573 | bookmark = {} 574 | bookmark["name"] = self.txtName.text() 575 | bookmark["path"] = self.txtLink.text() 576 | bookmark["dir"] = "" 577 | bookmark["icon"] = "" 578 | bookmark["openwith"] = "" 579 | return bookmark 580 | 581 | def accept(self): 582 | if self.txtName.text().strip() == "": 583 | QMessageBox.information(self, self.windowTitle(), self.tr("请填写网络链接的名称。")) 584 | self.txtName.setFocus(Qt.OtherFocusReason) 585 | return 586 | if self.txtLink.text().strip() == "": 587 | QMessageBox.information(self, self.windowTitle(), self.tr("请填写网络链接的地址。")) 588 | self.txtLink.setFocus(Qt.OtherFocusReason) 589 | return 590 | url = QUrl.fromUserInput(self.txtLink.text().strip()) 591 | if not url.isValid(): 592 | QMessageBox.information(self, self.windowTitle(), self.tr("您填写的似乎不是正确的网络链接地址。")) 593 | self.txtLink.setFocus(Qt.OtherFocusReason) 594 | self.txtLink.selectAll() 595 | return 596 | QDialog.accept(self) 597 | 598 | class Shortcut(Table): 599 | "快捷面板上的用户自定义图标" 600 | columns = {"id":"text", 601 | "name":"text", 602 | "path":"text", 603 | "openwith":"text", 604 | "dir":"text", 605 | "icon":"blob"} 606 | 607 | class ShortcutDatabase(Database): 608 | tables = (Shortcut, ) 609 | 610 | def createInitialData(self, table): 611 | if table is Shortcut: 612 | if os.name == "nt": 613 | self.insertShortcut({ 614 | "id": str(uuid.uuid4()), 615 | "name": self.tr("我的电脑"), 616 | "path": COMPUTER_PATH, 617 | "openwith": "", 618 | "icon": "", 619 | "dir": "", 620 | }) 621 | self.insertShortcut({ 622 | "id": str(uuid.uuid4()), 623 | "name": self.tr("我的文档"), 624 | "path": DOCUMENTS_PATH, 625 | "openwith": "", 626 | "icon": "", 627 | "dir": "", 628 | }) 629 | self.insertShortcut({ 630 | "id": str(uuid.uuid4()), 631 | "name": self.tr("我的音乐"), 632 | "path": MUSIC_PATH, 633 | "openwith": "", 634 | "icon": "", 635 | "dir": "", 636 | }) 637 | self.insertShortcut({ 638 | "id": str(uuid.uuid4()), 639 | "name": self.tr("图片收藏"), 640 | "path": PICTURES_PATH, 641 | "openwith": "", 642 | "icon": "", 643 | "dir": "", 644 | }) 645 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/machine_load.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ctypes 3 | import io 4 | from PyQt5.QtCore import QPoint, QRect, QTimer, Qt 5 | from PyQt5.QtGui import QPainter, QPen, QPolygon 6 | from PyQt5.QtWidgets import QWidget 7 | 8 | __all__ = ["MachineLoadWidget"] 9 | 10 | class MachineLoadWidget(QWidget): 11 | def __init__(self, parent): 12 | QWidget.__init__(self, parent) 13 | #self.setFrameStyle(QFrame.Box | QFrame.Sunken) 14 | self.timer = QTimer() 15 | self.timer.timeout.connect(self.collectMachineLoad) 16 | self.loads = [] 17 | self.maxLength = 400 18 | self.pointDistance = 5 #每点之间的间隔 19 | self.updateInterval = 500 #更新的时间间隔 20 | self.timer.setInterval(self.updateInterval) 21 | self.timer.start() 22 | self.machineLoad = MachineLoad.getInstance() 23 | self.boxWidth = 60 24 | 25 | def finalize(self): 26 | self.timer.stop() 27 | self.loads = [] 28 | 29 | def collectMachineLoad(self): 30 | rate = self.machineLoad.getLoad() 31 | self.loads.insert(0, rate) 32 | if len(self.loads) > self.maxLength: 33 | self.loads.pop( - 1) 34 | if self.isVisible(): 35 | self.update() 36 | 37 | def paintEvent(self, event): 38 | QWidget.paintEvent(self, event) 39 | width, height = self.width(), self.height() 40 | polygon = QPolygon() 41 | for i, rate in enumerate(self.loads): 42 | x = width - i * self.pointDistance 43 | y = height - rate * height 44 | if x < self.boxWidth: 45 | break 46 | polygon.append(QPoint(x, y)) 47 | painter = QPainter(self) 48 | pen = QPen() 49 | pen.setColor(Qt.darkGreen) 50 | painter.setPen(pen) 51 | painter.setRenderHint(QPainter.Antialiasing, True) 52 | #画网格 53 | painter.setOpacity(0.5) 54 | gridSize = self.pointDistance * 4 55 | deltaX = (width - self.boxWidth) % gridSize + self.boxWidth 56 | deltaY = height % gridSize 57 | for i in range(int(width / gridSize)): 58 | x = deltaX + gridSize * i 59 | painter.drawLine(x, 0, x, height) 60 | for j in range(int(height / gridSize)): 61 | y = j * gridSize + deltaY 62 | painter.drawLine(self.boxWidth, y, width, y) 63 | #画折线 64 | pen.setColor(Qt.darkCyan) 65 | pen.setWidth(2) 66 | painter.setPen(pen) 67 | painter.setOpacity(1) 68 | painter.drawPolyline(polygon) 69 | #画展示框 70 | if len(self.loads) > 0: 71 | rate = self.loads[0] 72 | else: 73 | rate = 1.0 74 | rect1 = QRect(4, height * 0.05, self.boxWidth - 9, height * 0.7) 75 | rect2 = QRect(4, height * 0.8, self.boxWidth - 9, height * 0.2) 76 | centerX = int(rect1.width() / 2) + 1 77 | pen.setWidth(1) 78 | for i in range(rect1.height()): 79 | if i % 4 == 0: 80 | continue 81 | if (rect1.height() - i) / rect1.height() > rate: 82 | pen.setColor(Qt.darkGreen) 83 | else: 84 | pen.setColor(Qt.green) 85 | painter.setPen(pen) 86 | for j in range(rect1.width()): 87 | if centerX - 1 <= j <= centerX + 1: 88 | continue 89 | painter.drawPoint(rect1.x() + j, rect1.y() + i) 90 | pen.setColor(Qt.black) 91 | painter.setPen(pen) 92 | painter.drawText(rect2, Qt.AlignHCenter | Qt.AlignVCenter, str(int(rate * 100)) + "%") 93 | # 94 | # points=int(self.width()/self.pointDistance) 95 | # if points>len(self.loads): 96 | # beginIndex=0 97 | # beginPoint=points-len(self.loads) 98 | # else: 99 | # beginIndex=len(self.loads)-points 100 | # beginPoint=0 101 | # j=0 102 | # height=self.height() 103 | # polygonGreen=QPolygon() 104 | # for i in range(beginIndex, len(self.loads)): 105 | # rate=self.loads[i] 106 | # x=(beginPoint+j)*self.pointDistance 107 | # y=height-rate*height 108 | # polygonGreen.append(QPoint(x, y)) 109 | # j+=1 110 | # painter=QPainter(self) 111 | # pen=QPen() 112 | # pen.setColor(Qt.green) 113 | # pen.setWidth(2) 114 | # painter.setPen(pen) 115 | # painter.setRenderHint(QPainter.Antialiasing, True) 116 | # painter.drawPolyline(polygonGreen) 117 | 118 | 119 | if os.name == "nt": 120 | import ctypes.wintypes 121 | class FILETIME(ctypes.Structure): 122 | _fields_ = [("dwLowDateTime", ctypes.wintypes.DWORD), 123 | ("dwHighDateTime", ctypes.wintypes.DWORD)] 124 | 125 | def __int__(self): 126 | return self.dwHighDateTime * 0x100000000 + self.dwLowDateTime 127 | 128 | GetSystemTimes = ctypes.windll.kernel32.GetSystemTimes 129 | 130 | class MachineLoad: 131 | _instance = None 132 | 133 | @staticmethod 134 | def getInstance(): 135 | if MachineLoad._instance is None: 136 | MachineLoad._instance = MachineLoad() 137 | return MachineLoad._instance 138 | 139 | def __init__(self): 140 | idle, kernel, user = FILETIME(), FILETIME(), FILETIME() 141 | GetSystemTimes(ctypes.byref(idle), ctypes.byref(kernel), ctypes.byref(user)) 142 | self.idle0, self.kernel0, self.user0 = int(idle), int(kernel), int(user) 143 | 144 | def getLoad(self): 145 | idle, kernel, user = FILETIME(), FILETIME(), FILETIME() 146 | GetSystemTimes(ctypes.byref(idle), ctypes.byref(kernel), ctypes.byref(user)) 147 | idle1, kernel1, user1 = int(idle), int(kernel), int(user) 148 | a, b, c = idle1 - self.idle0, kernel1 - self.kernel0, user1 - self.user0 149 | self.idle0, self.kernel0, self.user0 = idle1, kernel1, user1 150 | if (b + c) == 0: 151 | return 1 152 | return (b + c - a) / (b + c) 153 | 154 | elif os.path.exists("/proc/stat"): 155 | class MachineLoad: 156 | _instance = None 157 | lastStatics = None 158 | 159 | @staticmethod 160 | def getInstance(): 161 | if MachineLoad._instance is None: 162 | MachineLoad._instance = MachineLoad() 163 | return MachineLoad._instance 164 | 165 | def getLoad(self): 166 | try: 167 | with io.open("/proc/stat", encoding = "ascii") as statfile: 168 | firstline = statfile.readline() 169 | if firstline.endswith("\n"): 170 | firstline = firstline[:-1] 171 | statics = list(map(int, firstline.split()[1:8])) 172 | if self.lastStatics is None: 173 | self.lastStatics = statics 174 | return 0.0 175 | delta = list(map(lambda new, old: new - old, statics, self.lastStatics)) 176 | self.lastStatics = statics 177 | user, nice, system, idle, iowait, irq, softirq = delta 178 | return (sum(delta) - idle) / sum(delta) 179 | except: 180 | return 0 181 | 182 | else: 183 | class MachineLoad: 184 | _instance = None 185 | 186 | @staticmethod 187 | def getInstance(): 188 | if MachineLoad._instance is None: 189 | MachineLoad._instance = MachineLoad() 190 | return MachineLoad._instance 191 | 192 | def getLoad(self): 193 | return 0 194 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/quick_access.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QSize 2 | from PyQt5.QtWidgets import QFrame, QListView, QHBoxLayout 3 | 4 | __all__ = ["QuickAccessWidget"] 5 | 6 | class QuickAccessWidget(QFrame): 7 | def __init__(self, parent): 8 | QFrame.__init__(self, parent) 9 | self.setFrameStyle(QFrame.Box | QFrame.Sunken) 10 | self.setStyleSheet("QListView {background: transparent; }") 11 | self.setLayout(QHBoxLayout()) 12 | self.layout().setContentsMargins(0, 0, 0, 0) 13 | self.listView = QListView(self) 14 | self.layout().addWidget(self.listView) 15 | 16 | self.listView.setModel(self.window().quickAccessModel) 17 | self.listView.setMovement(QListView.Snap) 18 | self.listView.setFlow(QListView.LeftToRight) 19 | self.listView.setResizeMode(QListView.Adjust) 20 | gridSize = self.logicalDpiX() / 96 * 60 21 | self.listView.setGridSize(QSize(gridSize, gridSize)) 22 | self.listView.setViewMode(QListView.IconMode) 23 | 24 | self.listView.activated.connect(self.listView.model().runShortcut) 25 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/shortcut.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ShortcutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 462 10 | 267 11 | 12 | 13 | 14 | 创建快捷方式 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | QFormLayout::AllNonFixedFieldsGrow 23 | 24 | 25 | 26 | 27 | 名称: 28 | 29 | 30 | txtName 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 文件/程序: 41 | 42 | 43 | txtPath 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 浏览(P)... 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 编辑程序: 65 | 66 | 67 | txtOpenwith 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 浏览(&E)... 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 运行目录: 89 | 90 | 91 | txtDir 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 浏览(N)... 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 0 118 | 0 119 | 120 | 121 | 122 | 123 | 50 124 | 50 125 | 126 | 127 | 128 | 129 | 50 130 | 50 131 | 132 | 133 | 134 | Qt::NoFocus 135 | 136 | 137 | false 138 | 139 | 140 | 141 | 142 | 143 | 144 | 40 145 | 40 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | Qt::Vertical 154 | 155 | 156 | 157 | 20 158 | 40 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Qt::Vertical 171 | 172 | 173 | 174 | 20 175 | 9 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 184 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 185 | p, li { white-space: pre-wrap; } 186 | </style></head><body style=" font-family:'宋体'; font-size:9pt; font-weight:400; font-style:normal;"> 187 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">“编辑程序”与“运行目录”是选填项。</span></p> 188 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> 189 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Windows自动选择打开文件的方式,所以通常不必选择“编辑程序”。比如“文件/程序”填写的是一个一般的文档,Windows会自动使用“Microsoft Word”打开。如果您想要自行设定打开此文件的程序,比如用“金山WPS”,可以点击“浏览编辑程序”选择“金山WPS”。</p></body></html> 190 | 191 | 192 | true 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | Qt::Horizontal 202 | 203 | 204 | 205 | 40 206 | 20 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 默认运行目录(&U) 215 | 216 | 217 | 218 | 219 | 220 | 221 | 默认图标(&D) 222 | 223 | 224 | 225 | 226 | 227 | 228 | 更换图标(&I) 229 | 230 | 231 | 232 | 233 | 234 | 235 | 创建(&R) 236 | 237 | 238 | true 239 | 240 | 241 | 242 | 243 | 244 | 245 | 取消(&C) 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | btnOkay 257 | clicked() 258 | ShortcutDialog 259 | accept() 260 | 261 | 262 | 324 263 | 213 264 | 265 | 266 | 226 267 | 117 268 | 269 | 270 | 271 | 272 | btnCancel 273 | clicked() 274 | ShortcutDialog 275 | reject() 276 | 277 | 278 | 405 279 | 213 280 | 281 | 282 | 226 283 | 117 284 | 285 | 286 | 287 | 288 | btnFace 289 | clicked() 290 | btnChangeIcon 291 | click() 292 | 293 | 294 | 413 295 | 35 296 | 297 | 298 | 240 299 | 251 300 | 301 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/textpad.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QFrame, QPlainTextEdit, QHBoxLayout 2 | 3 | __all__ = ["TextpadWidget"] 4 | 5 | class TextpadWidget(QFrame): 6 | def __init__(self, parent): 7 | QFrame.__init__(self, parent) 8 | self.textpad = QPlainTextEdit(self) 9 | self.setLayout(QHBoxLayout()) 10 | self.layout().setContentsMargins(0, 0, 0, 0) 11 | self.layout().addWidget(self.textpad) 12 | 13 | self.setFrameStyle(QFrame.Box | QFrame.Sunken) 14 | self.setStyleSheet("QPlainTextEdit{background:transparent;}") 15 | 16 | settings = self.window().platform.getSettings() 17 | text = settings.value("textpad", "") 18 | self.textpad.setPlainText(text) 19 | 20 | def finalize(self): 21 | settings = self.window().platform.getSettings() 22 | settings.setValue("textpad", self.textpad.toPlainText()) 23 | 24 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/todo_backend.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from PyQt5.QtWidgets import QDialog, QMessageBox, QDialogButtonBox 3 | from besteam.utils.sql import Database, Table 4 | from .Ui_todo_editor import Ui_SimpleTodoEditor 5 | 6 | __all__ = ["SimpleBackend"] 7 | 8 | class SimpleTodo(Table): 9 | columns = {"id":"text", 10 | "finishment":"number", 11 | "subject":"text", 12 | } 13 | 14 | class SimpleTodoDatabas(Database): 15 | tables = (SimpleTodo, ) 16 | 17 | class SimpleBackend: 18 | def __init__(self, databaseFile): 19 | self.db = SimpleTodoDatabas(databaseFile) 20 | self.showAll = False 21 | 22 | def editTask(self, parent, task): 23 | d = SimpleTodoEditor(parent) 24 | quickpanel = parent.window() 25 | result = quickpanel.runDialog(d.edit, task) 26 | if result == QDialog.Accepted: 27 | task.update(d.getResult()) 28 | d.deleteLater() 29 | return True 30 | d.deleteLater() 31 | return False 32 | 33 | def createTodo(self, parent): 34 | d = SimpleTodoEditor(parent) 35 | quickpanel = parent.window() 36 | result = quickpanel.runDialog(d.create) 37 | if result == QDialog.Accepted: 38 | task = d.getResult() 39 | task["id"] = str(uuid.uuid4()) 40 | d.deleteLater() 41 | return self.db.insertSimpleTodo(task) 42 | d.deleteLater() 43 | return None 44 | 45 | def createTodoQuickly(self, subject): 46 | task = { 47 | "id": str(uuid.uuid4()), 48 | "subject": subject, 49 | "finishment": 0, 50 | } 51 | return self.db.insertSimpleTodo(task) 52 | 53 | def removeTodo(self, task): 54 | task.deleteFromDatabase() 55 | 56 | def listTodo(self): 57 | todoList = [] 58 | for todo in self.db.selectSimpleTodo(""): 59 | if not self.showAll and todo["finishment"] == 100: 60 | continue 61 | todoList.append(todo) 62 | return todoList 63 | 64 | def updateTaskById(self, taskId): 65 | #不需要刷新其它界面 66 | pass 67 | 68 | def setShowAll(self, showAll): 69 | self.showAll = showAll 70 | 71 | class SimpleTodoEditor(QDialog, Ui_SimpleTodoEditor): 72 | def __init__(self, parent): 73 | QDialog.__init__(self, parent) 74 | self.setupUi(self) 75 | 76 | def edit(self, task): 77 | self.setWindowTitle(self.tr("编辑待办事项")) 78 | self.txtSubject.setText(task["subject"]) 79 | if task["finishment"] == 0: 80 | self.rdoUnfinished.setChecked(True) 81 | elif task["finishment"] == 100: 82 | self.rdoFinished.setChecked(True) 83 | else: 84 | self.rdoProcessing.setChecked(True) 85 | try: 86 | return getattr(self, "exec")() 87 | except AttributeError: 88 | return getattr(self, "exec_")() 89 | 90 | def create(self): 91 | self.setWindowTitle(self.tr("创建待办事项")) 92 | btnSave = self.buttonBox.button(QDialogButtonBox.Save) 93 | btnSave.setText(self.tr("创建(&C)")) 94 | self.rdoUnfinished.setChecked(True) 95 | try: 96 | return getattr(self, "exec")() 97 | except AttributeError: 98 | return getattr(self, "exec_")() 99 | 100 | def accept(self): 101 | if self.txtSubject.text().strip() == "": 102 | QMessageBox.information(self, self.windowTitle(), self.tr("标题不能为空。")) 103 | return 104 | if not (self.rdoFinished.isChecked() or self.rdoProcessing.isChecked() or self.rdoUnfinished.isChecked()): 105 | QMessageBox.information(self, self.windowTitle(), self.tr("请选择完成度。")) 106 | return 107 | QDialog.accept(self) 108 | 109 | def getResult(self): 110 | task = {} 111 | task["subject"] = self.txtSubject.text().strip() 112 | if self.rdoFinished.isChecked(): 113 | task["finishment"] = 100 114 | elif self.rdoUnfinished.isChecked(): 115 | task["finishment"] = 0 116 | else: 117 | task["finishment"] = 50 118 | return task 119 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/todo_editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SimpleTodoEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 348 10 | 142 11 | 12 | 13 | 14 | 编辑待办事项 15 | 16 | 17 | 18 | 19 | 20 | QFormLayout::AllNonFixedFieldsGrow 21 | 22 | 23 | 24 | 25 | 标题: 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 完成度: 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 未开始 45 | 46 | 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | 进行中 55 | 56 | 57 | 58 | 59 | 60 | 61 | 已完成 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | QDialogButtonBox::Cancel|QDialogButtonBox::Save 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | buttonBox 82 | accepted() 83 | SimpleTodoEditor 84 | accept() 85 | 86 | 87 | 173 88 | 120 89 | 90 | 91 | 173 92 | 70 93 | 94 | 95 | 96 | 97 | buttonBox 98 | rejected() 99 | SimpleTodoEditor 100 | reject() 101 | 102 | 103 | 173 104 | 120 105 | 106 | 107 | 173 108 | 70 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/todo_list.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, pyqtSignal 2 | from PyQt5.QtGui import QCursor 3 | from PyQt5.QtWidgets import QComboBox, QHeaderView, QMenu, QMessageBox, QStyledItemDelegate, \ 4 | QWidget 5 | from .todo_backend import SimpleBackend 6 | from .Ui_todolist import Ui_TodoListWidget 7 | 8 | __all__ = ["TodoListWidget"] 9 | 10 | def setListValue(listWidget, value): 11 | "设置QComboBox的值" 12 | for i in range(listWidget.count()): 13 | if listWidget.itemText(i) == value: 14 | listWidget.setCurrentIndex(i) 15 | return 16 | if listWidget.isEditable(): 17 | listWidget.setEditText(value) 18 | 19 | class TodoListWidget(QWidget, Ui_TodoListWidget): 20 | def __init__(self, parent): 21 | QWidget.__init__(self, parent) 22 | self.setupUi(self) 23 | self.backend = SimpleBackend(parent.window().platform.databaseFile) 24 | self.todoListModel = TodoListModel() 25 | self.todoListDelegate = TodoListDelegate() 26 | self.todoListModel.updateTodoList(self.backend.listTodo()) 27 | self.tvTodoList.setModel(self.todoListModel) 28 | self.tvTodoList.header().setSectionResizeMode(QHeaderView.ResizeToContents) 29 | self.tvTodoList.setItemDelegate(self.todoListDelegate) 30 | self.makeConnections() 31 | self.setStyleSheet("QTreeView {background:transparent;}") 32 | 33 | def makeConnections(self): 34 | self.tvTodoList.customContextMenuRequested.connect(self.onTodoListContextMenuReqeusted) 35 | self.actionCreateTodo.triggered.connect(self.createTodo) 36 | self.actionEditTodo.triggered.connect(self.editTodo) 37 | self.actionRemoveTodo.triggered.connect(self.removeTodo) 38 | self.actionModifyTodoSubject.triggered.connect(self.modifyTodoSubject) 39 | self.actionMarkFinished.triggered.connect(self.markFinished) 40 | self.actionMarkUnfinished.triggered.connect(self.markUnfinished) 41 | self.actionMarkProcessing.triggered.connect(self.markProcessing) 42 | self.btnAddTodo.clicked.connect(self.addTodoQuickly) 43 | self.todoListModel.taskUpdated.connect(self.backend.updateTaskById) 44 | self.chkShowAll.toggled.connect(self.setShowAll) 45 | 46 | def setShowAll(self, showAll): 47 | self.backend.setShowAll(showAll) 48 | self.todoListModel.updateTodoList(self.backend.listTodo()) 49 | 50 | def showEvent(self, event): 51 | self.todoListModel.updateTodoList(self.backend.listTodo()) 52 | QWidget.showEvent(self, event) 53 | 54 | def onTodoListContextMenuReqeusted(self, pos): 55 | index = self.tvTodoList.indexAt(pos) 56 | if index.isValid(): 57 | self.tvTodoList.setCurrentIndex(index) 58 | menu = QMenu() 59 | if index.isValid(): 60 | task = self.todoListModel.taskAt(index) 61 | if task["finishment"] == 0: 62 | menu.addAction(self.actionMarkProcessing) 63 | menu.addAction(self.actionMarkFinished) 64 | elif task["finishment"] < 100: 65 | menu.addAction(self.actionMarkUnfinished) 66 | menu.addAction(self.actionMarkFinished) 67 | else: 68 | menu.addAction(self.actionMarkUnfinished) 69 | menu.addAction(self.actionMarkProcessing) 70 | menu.addSeparator() 71 | menu.addAction(self.actionCreateTodo) 72 | if index.isValid(): 73 | menu.addAction(self.actionRemoveTodo) 74 | menu.addAction(self.actionModifyTodoSubject) 75 | menu.addAction(self.actionEditTodo) 76 | try: 77 | getattr(menu, "exec")(QCursor.pos()) 78 | except AttributeError: 79 | getattr(menu, "exec_")(QCursor.pos()) 80 | 81 | def editTodo(self): 82 | currentIndex = self.tvTodoList.currentIndex() 83 | task = self.todoListModel.taskAt(currentIndex) 84 | if task is None: 85 | return 86 | if self.backend.editTask(self, task): 87 | self.todoListModel.updateTodo(currentIndex) 88 | 89 | def createTodo(self): 90 | task = self.backend.createTodo(self) 91 | if task is None: 92 | return 93 | index = self.todoListModel.appendTodo(task) 94 | if index.isValid(): 95 | self.tvTodoList.setCurrentIndex(index) 96 | 97 | def removeTodo(self): 98 | currentIndex = self.tvTodoList.currentIndex() 99 | if not currentIndex.isValid(): 100 | return 101 | self.backend.removeTodo(self.todoListModel.todoAt(currentIndex)) 102 | self.todoListModel.removeTodo(currentIndex) 103 | 104 | def modifyTodoSubject(self): 105 | currentIndex = self.tvTodoList.currentIndex() 106 | if not currentIndex.isValid(): 107 | return 108 | currentIndex = self.todoListModel.index(currentIndex.row(), 1, QModelIndex()) 109 | self.tvTodoList.edit(currentIndex) 110 | 111 | def markFinished(self): 112 | self.markTodoState(100) 113 | 114 | def markUnfinished(self): 115 | self.markTodoState(0) 116 | 117 | def markProcessing(self): 118 | self.markTodoState(50) 119 | 120 | def markTodoState(self, finishment, currentIndex = None): 121 | if currentIndex is None: 122 | currentIndex = self.tvTodoList.currentIndex() 123 | if not currentIndex.isValid(): 124 | return 125 | task = self.todoListModel.taskAt(currentIndex) 126 | task["finishment"] = finishment 127 | self.todoListModel.updateTodo(currentIndex) 128 | self.backend.updateTaskById(task["id"]) 129 | 130 | def addTodoQuickly(self): 131 | subject = self.txtTodoSubject.text().strip() 132 | if subject == "": 133 | QMessageBox.information(self, self.tr("添加待办事项"), self.tr("不能添加空的待办事项。")) 134 | return 135 | task = self.backend.createTodoQuickly(subject) 136 | index = self.todoListModel.appendTodo(task) 137 | self.tvTodoList.setCurrentIndex(index) 138 | self.txtTodoSubject.setText("") 139 | self.txtTodoSubject.setFocus(Qt.OtherFocusReason) 140 | 141 | 142 | class TodoListModel(QAbstractTableModel): 143 | taskUpdated = pyqtSignal(str) #当用户直接编辑待办事项的主题时引发这个信号,其中的参数是待办事项的ID 144 | 145 | def __init__(self): 146 | QAbstractTableModel.__init__(self) 147 | self.todoList = [] 148 | 149 | def rowCount(self, parent): 150 | if not parent.isValid(): 151 | return len(self.todoList) 152 | return 0 153 | 154 | def columnCount(self, parent): 155 | if not parent.isValid(): 156 | return 2 157 | return 0 158 | 159 | def data(self, index, role): 160 | if not index.isValid(): 161 | return None 162 | if index.column() == 1 and role in (Qt.DisplayRole, Qt.EditRole): 163 | return self.todoList[index.row()]["subject"] 164 | elif index.column() == 0 and role in (Qt.DisplayRole, Qt.EditRole): 165 | finishment = self.todoList[index.row()]["finishment"] 166 | if finishment == 0: 167 | state = self.tr("未开始") 168 | elif finishment == 100: 169 | state = self.tr("已完成") 170 | else: 171 | state = self.tr("进行中") 172 | return state 173 | return None 174 | 175 | def setData(self, index, value, role): 176 | if role == Qt.EditRole and index.isValid(): 177 | task = self.todoList[index.row()] 178 | if index.column() == 0: 179 | if value == self.tr("未开始"): 180 | task["finishment"] = 0 181 | elif value == self.tr("已完成"): 182 | task["finishment"] = 100 183 | else: 184 | task["finishment"] = 50 185 | else: 186 | assert index.column() == 1 187 | task["subject"] = value 188 | self.dataChanged.emit(index, index) 189 | self.taskUpdated.emit(task["id"]) 190 | return True 191 | return QAbstractTableModel.setData(self, index, value, role) 192 | 193 | def flags(self, index): 194 | if index.isValid(): 195 | return Qt.ItemIsEditable | QAbstractTableModel.flags(self, index) 196 | return QAbstractTableModel.flags(self, index) 197 | 198 | def headerData(self, section, orientation, role): 199 | if orientation == Qt.Horizontal and role == Qt.DisplayRole: 200 | if section == 0: 201 | return self.tr("完成状态") #注意标题宽度要大于编辑完成状态时显示的QComboBox 202 | elif section == 1: 203 | return self.tr("标题") 204 | return None 205 | 206 | def updateTodoList(self, todoList): 207 | "更新待办事项列表。当快捷面板被显示时,刷新列表内容。" 208 | self.beginResetModel() 209 | self.todoList = todoList 210 | self.endResetModel() 211 | 212 | def todoAt(self, index): 213 | assert index.isValid() 214 | return self.todoList[index.row()] 215 | 216 | def removeTodo(self, index): 217 | self.beginRemoveRows(QModelIndex(), index.row(), index.row()) 218 | self.todoList.pop(index.row()) 219 | self.endRemoveRows() 220 | 221 | def updateTodo(self, index): 222 | topLeft = self.createIndex(index.row(), 0) 223 | bottomRight = self.createIndex(index.row(), 1) 224 | self.dataChanged.emit(topLeft, bottomRight) 225 | 226 | def taskAt(self, index): 227 | return self.todoList[index.row()] 228 | 229 | def appendTodo(self, task): 230 | self.beginInsertRows(QModelIndex(), len(self.todoList), len(self.todoList)) 231 | self.todoList.append(task) 232 | self.endInsertRows() 233 | return self.createIndex(len(self.todoList) - 1, 0) 234 | 235 | 236 | class TodoListDelegate(QStyledItemDelegate): 237 | "待办事项列表第一列是状态,需要使用QComboBox进行选择。所以设置自定义的Delegate" 238 | def createEditor(self, parent, option, index): 239 | if index.isValid() and index.column() == 0: 240 | finishmentWidget = QComboBox(parent) 241 | finishmentWidget.addItems([self.tr("已完成"), self.tr("进行中"), self.tr("未完成")]) 242 | return finishmentWidget 243 | return QStyledItemDelegate.createEditor(self, parent, option, index) 244 | 245 | def setEditorData(self, widget, index): 246 | if index.isValid() and index.column() == 0: 247 | setListValue(widget, index.data(Qt.EditRole)) 248 | return 249 | QStyledItemDelegate.setEditorData(self, widget, index) 250 | 251 | def setModelData(self, editor, model, index): 252 | if index.isValid() and index.column() == 0: 253 | model.setData(index, editor.currentText(), Qt.EditRole) 254 | return 255 | QStyledItemDelegate.setModelData(self, editor, model, index) 256 | -------------------------------------------------------------------------------- /besteam/im/quick_panel/widgets/todolist.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TodoListWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 407 10 | 289 11 | 12 | 13 | 14 | 待办事项 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | Qt::CustomContextMenu 24 | 25 | 26 | QFrame::Box 27 | 28 | 29 | false 30 | 31 | 32 | true 33 | 34 | 35 | false 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 添加 51 | 52 | 53 | 54 | :/images/task-new.png:/images/task-new.png 55 | 56 | 57 | 58 | 59 | 60 | 61 | 显示完成项 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | :/images/edit.png:/images/edit.png 72 | 73 | 74 | 编辑/查看待办事项(&E) 75 | 76 | 77 | 编辑/查看待办事项 78 | 79 | 80 | 编辑/查看待办事项 81 | 82 | 83 | 84 | 85 | 86 | :/images/new.png:/images/new.png 87 | 88 | 89 | 添加待办事项(&A) 90 | 91 | 92 | 添加待办事项 93 | 94 | 95 | 添加待办事项 96 | 97 | 98 | 99 | 100 | 101 | :/images/delete.png:/images/delete.png 102 | 103 | 104 | 删除待办事项(&R) 105 | 106 | 107 | 删除待办事项 108 | 109 | 110 | 删除待办事项 111 | 112 | 113 | 114 | 115 | 116 | :/images/edit-rename.png:/images/edit-rename.png 117 | 118 | 119 | 修改待办事项标题(&T) 120 | 121 | 122 | 修改待办事项标题 123 | 124 | 125 | 修改待办事项标题 126 | 127 | 128 | 129 | 130 | 标记为"进行中"(&P) 131 | 132 | 133 | 标记为"进行中" 134 | 135 | 136 | 标记为"进行中" 137 | 138 | 139 | 140 | 141 | 标记为"已完成" 142 | 143 | 144 | 145 | 146 | 标记为"未开始" 147 | 148 | 149 | 150 | 151 | 152 | 153 | txtTodoSubject 154 | returnPressed() 155 | btnAddTodo 156 | animateClick() 157 | 158 | 159 | 140 160 | 274 161 | 162 | 163 | 345 164 | 274 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /besteam/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/besteam/utils/__init__.py -------------------------------------------------------------------------------- /besteam/utils/globalkey.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | __all__ = ["GlobalKey"] 5 | 6 | if sys.platform == "win32": 7 | from .winglobalkey import GlobalKey 8 | else: 9 | try: 10 | from PyKDE5.kdeui import KAction; KAction 11 | from .kdeglobalkey import GlobalKey 12 | except ImportError: 13 | from PyQt5.QtCore import pyqtSignal, QObject 14 | 15 | class GlobalKey(QObject): 16 | catched = pyqtSignal(int) 17 | 18 | def __init__(self): 19 | super(GlobalKey, self).__init__() 20 | self.nextId = 0 21 | 22 | def close(self): 23 | pass 24 | 25 | def addHotKey(self, name, key): 26 | self.nextId += 1 27 | return self.nextId 28 | 29 | def removeHotKey(self, keyId): 30 | pass 31 | -------------------------------------------------------------------------------- /besteam/utils/kdeglobalkey.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtSignal, QObject 2 | from PyKDE5.kdeui import KAction, KGlobalAccel, KShortcut 3 | 4 | class GlobalKey(QObject): 5 | catched = pyqtSignal(int) 6 | 7 | def __init__(self): 8 | super(QObject, self).__init__() 9 | self.nextId = 0 10 | self.actions = {} 11 | 12 | def close(self): 13 | for action in self.actions.values(): 14 | action.setGlobalShortcutAllowed(False, KAction.NoAutoloading) 15 | #action.forgetGlobalShortcut() 16 | self.actions.clear() 17 | 18 | def addHotKey(self, name, key): 19 | if not KGlobalAccel.isGlobalShortcutAvailable(key): 20 | actions = KGlobalAccel.getGlobalShortcutsByKey(key) 21 | if KGlobalAccel.promptStealShortcutSystemwide(None, actions, key): 22 | KGlobalAccel.stealShortcutSystemwide(key) 23 | action = KAction(None) 24 | action.setObjectName(name) 25 | action.setText(name) 26 | action.setGlobalShortcut(KShortcut(key), \ 27 | KAction.ShortcutType(KAction.ActiveShortcut | KAction.DefaultShortcut), 28 | KAction.NoAutoloading) 29 | action.triggered.connect(self.catchGlobalKey) 30 | self.actions[self.nextId] = action 31 | self.nextId += 1 32 | return self.nextId - 1 33 | 34 | def removeHotKey(self, keyId): 35 | if keyId not in self.actions: 36 | return 37 | action = self.actions.pop(keyId) 38 | action.setGlobalShortcutAllowed(False, KAction.NoAutoloading) 39 | #action.forgetGlobalShortcut() 40 | 41 | def catchGlobalKey(self, *args): 42 | sender = self.sender() 43 | for keyId, action in self.actions.items(): 44 | if action is sender: 45 | self.catched.emit(keyId) 46 | return 47 | -------------------------------------------------------------------------------- /besteam/utils/settings.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, QTimer 2 | 3 | import pickle 4 | from besteam.utils.sql import transaction, Table, Database 5 | 6 | class Preference(Table): 7 | pkName = "key" 8 | columns = {"key":"text", 9 | "value":"blob"} 10 | 11 | class PreferenceDatabase(Database): 12 | tables = (Preference, ) 13 | 14 | class _Settings(QObject): 15 | class Item: 16 | key = None 17 | value = None 18 | dirty = False 19 | 20 | def __init__(self, db): 21 | super(_Settings, self).__init__() 22 | if type(db) is PreferenceDatabase: 23 | self.db = db 24 | else: 25 | self.db = PreferenceDatabase(db) 26 | self.preferences = [] 27 | for row in self.db.selectPreference(""): 28 | item = _Settings.Item() 29 | item.key = row["key"] 30 | item.value = pickle.loads(row["value"]) 31 | self.preferences.append(item) 32 | if QTimer: 33 | self.autoSaveTimer = QTimer() 34 | self.autoSaveTimer.timeout.connect(self.save) 35 | self.autoSaveTimer.start(5000) 36 | 37 | def __del__(self): 38 | self.save() 39 | 40 | def contains(self, key): 41 | for item in self.preferences: 42 | if item.key == key: 43 | return True 44 | return False 45 | 46 | def getPreference(self, key): 47 | for item in self.preferences: 48 | if item.key == key: 49 | return item.value 50 | raise KeyError() 51 | 52 | def setPreference(self, key, value): 53 | for item in self.preferences: 54 | if item.key == key: 55 | item.value = value 56 | item.dirty = True 57 | return True 58 | item = _Settings.Item() 59 | item.key = key 60 | item.value = value 61 | item.dirty = True 62 | self.preferences.append(item) 63 | return True 64 | 65 | def getPreferenceKeysWithPrefix(self, prefix): 66 | #XXX 是否包含子目录的键值呢?对照QSettings,应该是不包含的 67 | assert prefix.endswith("/") 68 | keys = [] 69 | for item in self.preferences: 70 | if item.key.startswith(prefix): 71 | name = item.key[len(prefix):] 72 | if "/" not in name: 73 | keys.append(name) 74 | return keys 75 | 76 | @transaction 77 | def removePreference(self, key): 78 | for item in self.preferences: 79 | if item.key == key: 80 | self.preferences.remove(item) 81 | self.db.deletePreference("where key=?", key) 82 | return 83 | raise KeyError() 84 | 85 | @transaction 86 | def save(self): 87 | for item in self.preferences: 88 | if not item.dirty: 89 | continue 90 | self.db.deletePreference("where key=?", item.key) 91 | self.db.insertPreference({"key":item.key, "value":pickle.dumps(item.value, 2)}) 92 | item.dirty = False 93 | 94 | class Settings: 95 | """供各个模块存储用户使用偏好,比如窗口大小,显示的字段等数据。 96 | 与QSettings类似,数据被抽象成文件夹 97 | 在使用前需要使用beginGroup与endGroup来定位文件夹,然后才能读取数据 98 | 所有的数据存储在数据库中。使用方法如: 99 | >>> settings = Settings() 100 | 101 | >>> settings.beginGroup("/appearance") 102 | >>> settings.value("style", "plastique") 103 | 'phase' 104 | >>> settings.endGroup() 105 | """ 106 | def __init__(self, _settings): 107 | "_Settings读取/保存用户配置。而Settings类则提供了类似于QSettings的访问界面" 108 | self._settings = _settings 109 | self.prefix = [] 110 | 111 | def duplicate(self): 112 | """创建一个新的Settings对象,不包含当前Settings的任何状态。方便没有userService时使用 113 | 使用场景参见im.chat.ChatWindowController.loadSettings()""" 114 | return Settings(self._settings) 115 | 116 | def sync(self): 117 | self._settings.save() 118 | 119 | def beginGroup(self, groupName): 120 | """定位到某个路径,接下来的读取与存储操作都定位于这个路径。 121 | groupName可以是绝对路径也可以是相对路径。默认的路径是'/'。""" 122 | if groupName.startswith("/"): 123 | self.prefix = [groupName[1:]] 124 | else: 125 | self.prefix.append(groupName) 126 | 127 | def endGroup(self): 128 | assert len(self.prefix) > 0 129 | self.prefix.pop() 130 | 131 | def _prefix(self): 132 | "返回当前路径的全路径名" 133 | return "/" + "".join([groupName + "/" for groupName in self.prefix]) 134 | 135 | def keys(self): 136 | prefix = self._prefix() 137 | return self._settings.getPreferenceKeysWithPrefix(prefix) 138 | 139 | def setValue(self, k, v): 140 | key = self._prefix() + k 141 | self._settings.setPreference(key, v) 142 | 143 | def value(self, k, default = None): 144 | try: 145 | key = self._prefix() + k 146 | return self._settings.getPreference(key) 147 | except KeyError: 148 | return default 149 | 150 | def contains(self, k): 151 | key = self._prefix() + k 152 | return self._settings.contains(key) 153 | 154 | def remove(self, k): 155 | key = self._prefix() + k 156 | try: 157 | self._settings.removePreference(key) 158 | except KeyError: 159 | return False 160 | else: 161 | return True 162 | 163 | def __enter__(self): 164 | return self 165 | 166 | def __exit__(self, exc_type, exc_value, traceback): 167 | self.sync() 168 | -------------------------------------------------------------------------------- /besteam/utils/sql.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import threading 4 | import pickle 5 | import traceback 6 | import types 7 | import sys 8 | import warnings 9 | import logging 10 | import functools 11 | try: 12 | from PyQt5.QtCore import QDate, QDateTime, QObject 13 | usingPyQt5 = True 14 | except ImportError: 15 | usingPyQt5 = False 16 | 17 | __all__ = ["Table", "Database", "DatabaseException", "transaction", "DataObject", 18 | "DataObjectProxy", "createDataObject", "createDetachedDataObject"] 19 | 20 | #是否打印调试信息,如果为真,会打印出所有执行的SQL语句 21 | sql_debug = False 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def pickle_dumps(o): 26 | return pickle.dumps(o, 2) 27 | 28 | def pickle_loads(s): 29 | return pickle.loads(s) 30 | 31 | class DictProxy: 32 | def setTarget(self, dataObject): 33 | self.__target = dataObject 34 | 35 | def target(self): 36 | return self.__target 37 | 38 | def __setitem__(self, k, v): 39 | self.__target[k] = v 40 | 41 | def __getitem__(self, k): 42 | return self.__target[k] 43 | 44 | def __delitem__(self, k): 45 | del self.__target[k] 46 | 47 | def get(self, k, default = None): 48 | try: 49 | return self.__getitem__(k) 50 | except KeyError: 51 | return default 52 | 53 | def update(self, d): 54 | if __debug__: 55 | if self.__setitem__.__func__ is not DictProxy.__setitem__.__func__ \ 56 | and self.update.__func__ is DictProxy.update.__func__: 57 | warnings.warn("是不是忘了重载DictProxy.update()") 58 | self.__target.update(d) 59 | 60 | def copy(self): 61 | return self.__target.copy() 62 | 63 | def __len__(self): 64 | return len(self.__target) 65 | 66 | def items(self): 67 | return self.__target.items() 68 | 69 | def keys(self): 70 | return self.__target.keys() 71 | 72 | def values(self): 73 | return self.__target.values() 74 | 75 | def __contains__(self, k): 76 | return k in self.__target 77 | 78 | def classNameToSqlName(className): 79 | """把表格的类名转换成表格的SQL表名。如`ClassName`到`class_name`""" 80 | return className[0].lower() + "".join( 81 | c if c.islower() else "_" + c.lower() for c in className[1:]) 82 | 83 | #让sqlite3认识QDateTime 84 | def adapt_QDateTime(d): 85 | return d.toTime_t() 86 | 87 | def convert_QDateTime(t): 88 | return QDateTime.fromTime_t(int(t)) 89 | 90 | def adapt_QDate(d): 91 | return d.toJulianDay() 92 | 93 | def convert_QDate(t): 94 | return QDate.fromJulianDay(int(t)) 95 | 96 | def adapt_bool(b): 97 | return 'T' if b else 'F' 98 | 99 | def convert_bool(b): 100 | return b == b"T" 101 | 102 | if usingPyQt5: 103 | sqlite3.register_adapter(QDateTime, adapt_QDateTime) 104 | sqlite3.register_adapter(QDate, adapt_QDate) 105 | sqlite3.register_adapter(bool, adapt_bool) 106 | sqlite3.register_adapter(list, pickle_dumps) 107 | sqlite3.register_adapter(dict, pickle_dumps) 108 | if usingPyQt5: 109 | sqlite3.register_converter("QDateTime", convert_QDateTime) 110 | sqlite3.register_converter("QDate", convert_QDate) 111 | sqlite3.register_converter("bool", convert_bool) 112 | sqlite3.register_converter("list", pickle.loads) 113 | sqlite3.register_converter("dict", pickle.loads) 114 | 115 | transaction_local = threading.local() 116 | __transaction_debug = False 117 | 118 | def transaction(wrapped): 119 | "标注某个函数是形成一个事务,可以嵌套,但是不形成子事务。" 120 | def wrapper(*l, **d): 121 | passed = False 122 | try: 123 | if hasattr(transaction_local, "transaction") or __transaction_debug: 124 | passed = True 125 | else: 126 | transaction_local.transaction = True 127 | transaction_local.conn = None 128 | result = wrapped(*l, **d) 129 | if not passed and transaction_local.conn is not None: 130 | transaction_local.conn.commit() 131 | return result 132 | except: 133 | if not passed and transaction_local.conn is not None: 134 | try: 135 | transaction_local.conn.rollback() 136 | except: 137 | if __debug__: 138 | traceback.print_exc() 139 | raise 140 | finally: 141 | if not passed: 142 | del transaction_local.transaction 143 | del transaction_local.conn 144 | functools.update_wrapper(wrapper, wrapped) 145 | return wrapper 146 | 147 | 148 | class DatabaseException(Exception): 149 | pass 150 | 151 | 152 | class InvalidTableException(DatabaseException): 153 | pass 154 | 155 | 156 | #DataObject是一个简单的东西。它并不能处理关系映射之类的东西。它不是什么ORM。 157 | #TODO 通过Database.update()语句更新数据库时没有判断数据对象需不需要更新。 158 | #这种情况可能会产生一些不一致现象。现在的原则是,使用数据对象就不使用update() 159 | class DataObject(DictProxy): 160 | """数据对象用于存取数据库记录,它的使用形式类似于Python内置的dict类型。 161 | DataObject本质是一个容纳数据库记录的缓存,但是可以设定某个字段不读取到内存中。 162 | 使用__setitem__()时,如果字段是数据库字段,该数值会立即保存到数据库内。 163 | 字段不是数据库字段,该数值会保存到缓存内,可以使用__getitem__取出这个数值 164 | 提供了一个deleteFromDatabase()函数用于从数据库中删除此条记录。 165 | 除了可以通过__getitem__()获得数据库字段,还有以下五个属性: 166 | DataObject.id 主键的值 167 | DataObject.table 数据对象所属的表格的Python类对象。 168 | DataObject.db 数据对象所属的Database 169 | DataObject.detached 数据对象是否处于分离状态 170 | DataObject.notInMemory 这个是一个列表,用于指明哪些字段不处于内存中 171 | 数据对象有两种状态————与数据库关联或者从数据库分离。当它处于与数据库关联的状态时, 172 | 使用__setitem__()或者update()设置的字段值会立即更新到数据库内。 173 | 当它处于分离状态时,字段值只会保存在缓存内。可以使用attach()方法将分离状态转变为 174 | 关联状态。调用attach()之后,处于缓存内的数据值会立即更新到数据库。 175 | 有时某些字段的值比较大,可能是一篇文章或者一个图像。这种字段不适宜放在缓存中。 176 | 可以将它的字段名加入到DataObject.notInMemory列表内。 177 | """ 178 | 179 | def __init__(self, id, table, db, record = None): 180 | self.id, self.table, self.db = id, table, db 181 | #如果detached为True,使用数据对象的__setitem__更新数据的时候,数据不会直接更新到数据库中 182 | self.detached = False 183 | self.notInMemory = [] 184 | self.convertors = {} 185 | if record is None: 186 | self.reload() 187 | else: 188 | self.setTarget(record) 189 | 190 | def __repr__(self): 191 | return "DataObject" + ("(detached):" if self.detached else ":") + repr(self.target()) 192 | 193 | def __getitem__(self, k): 194 | if k in self.notInMemory and not self.detached: 195 | cursor = self.db.conn().cursor() 196 | sql = "select %s from %s where %s=?;" % (k, self.table.getName(), self.table.getPkName()) 197 | id = self.target()[self.table.getPkName()] 198 | if sql_debug: 199 | print(sql, "id=", id) 200 | cursor.execute(sql, (id, )) 201 | row = cursor.fetchone() 202 | if row is None: 203 | raise KeyError 204 | v = row[0] 205 | if sys.version_info[0] < 3 and isinstance(v, buffer): 206 | v = bytes(v) 207 | else: 208 | v = DictProxy.__getitem__(self, k) 209 | if k in self.convertors: 210 | return self.convertors[k](v) 211 | return v 212 | 213 | @transaction 214 | def __setitem__(self, k, v): 215 | #一个小的优化,如果新值与旧值一样,就不写数据库 216 | changed = True 217 | if k not in self.notInMemory: # and not self.detached 218 | if k in self.target(): 219 | #有时候升级数据库的时候,旧的字段类型可能会和新的字段类型不一样。 220 | changed = (type(self.target()[k] is not type(v) or self.target()[k] != v)) 221 | self.target()[k] = v 222 | if not self.detached and changed: 223 | #实际上并不一定会更新,Database.update()会判断字段是不是数据库的字段 224 | self.db.update(self.table.getName(), {k:v, "__reload_cache":False}, \ 225 | "where %s=?" % self.table.getPkName(), self.id) 226 | 227 | @transaction 228 | def update(self, d): 229 | d = d.copy() 230 | self.target().update(d) 231 | if not self.detached: 232 | for field in self.notInMemory: 233 | try: 234 | del self.target()[field] 235 | except KeyError: 236 | pass 237 | d["__reload_cache"] = False 238 | self.db.update(self.table.getName(), d, "where %s=?" % self.table.getPkName(), self.id) 239 | 240 | @transaction 241 | def deleteFromDatabase(self): 242 | "从数据库中删除此纪录。" 243 | if self.detached: 244 | return 245 | self.db.delete(self.table.getName(), "where %s=?" % self.table.getPkName(), self.id) 246 | self.detached = True 247 | 248 | def reload(self): 249 | "重新载入所有数据。如果原来定义了notInMemory,最好不要使用reload(),会导致所有数据载入内存" 250 | #目前,用到这个函数并不多。这个函数原来设计为让数据库自动刷新缓存的。但是dolphin都是一些简单 251 | #的CRUD,根本用不到那些复杂的功能。 252 | if self.detached: 253 | return 254 | rows = self.db.select(self.table.getName(), "where %s=?" % self.pk, self.id) 255 | assert len(rows) == 1 256 | self.setTarget(rows[0]) 257 | 258 | def copy(self): 259 | "返回一个dict,内容是该条记录,包含self.notInMemory内的字段" 260 | d = self.target().copy() 261 | #TODO 优化一下,一次性把所有在notInMemory的字段取出来。不是很重要,因为目前大多数表格只有一个notInMemory字段。 262 | if not self.detached: 263 | for k in self.notInMemory: 264 | d[k] = self.__getitem__(k) 265 | #支持convertor 266 | for name, convertor in self.convertors.items(): 267 | d[name] = convertor(d[name]) 268 | return d 269 | 270 | def attach(self): 271 | "关联到数据库。把数据插入数据库。" 272 | if not self.detached: 273 | return 274 | self.db.insert(self.table.getName(), self.target()) 275 | for field in self.notInMemory: 276 | try: 277 | del self.target()[field] 278 | except KeyError: 279 | pass 280 | self.detached = False 281 | 282 | def createDataObject(dataObjectNameOrClass, db, record): 283 | """创建一个数据对象,除了本模块外,通常不直接使用DataObject的构造函数 284 | 参数dataObjectName是数据对象的名字,比如DiaryDay之类的。或者直接可以是类型 285 | db是数据对象从属的数据库,record则是数据对象的值""" 286 | if type(dataObjectNameOrClass) is types.ClassType and\ 287 | issubclass(dataObjectNameOrClass, Table): 288 | table = dataObjectNameOrClass 289 | else: 290 | assert isinstance(dataObjectNameOrClass, str) 291 | table = db.getTableByClassName(dataObjectNameOrClass) 292 | assert table is not None 293 | id = record[table.getPkName()] 294 | return DataObject(id, table, db, record) 295 | 296 | def createDetachedDataObject(dataObjectNameOrClass, db, record): 297 | """一个方便使用的小类。与createDataObject()类型。但是返回的类型没有关联到数据库。 298 | 修改数据时不会更新到数据库。""" 299 | do = createDataObject(dataObjectNameOrClass, db, record) 300 | do.detached = True 301 | return do 302 | 303 | class DataObjectProxy(DictProxy): 304 | """如果一个类想要基于DataObject实现dict的接口,可以继承这个类型。 305 | 不直接继承DataObject,因为它不是一种 is 关系。""" 306 | 307 | def deleteFromDatabase(self): 308 | self.target().deleteFromDatabase() 309 | 310 | def reload(self): 311 | self.target().reload() 312 | 313 | def setTarget(self, target): 314 | assert isinstance(target, DataObject) 315 | DictProxy.setTarget(self, target) 316 | 317 | if usingPyQt5: 318 | _QObject = QObject 319 | else: 320 | class _QObject: 321 | def __init__(self): 322 | pass 323 | 324 | def trUtf8(self, utf8Bytes): 325 | return utf8Bytes.decode("utf-8") 326 | 327 | #继承于QObject的主要原因是为了使用self.tr() 328 | class Database(_QObject): 329 | @transaction 330 | def __init__(self, dbfile): 331 | _QObject.__init__(self) 332 | self.dbfile = dbfile 333 | conn = self.conn() 334 | cursor = conn.cursor() 335 | #首先创建表格。创建表格的时候使用createInitialData()方法填充基本数据。 336 | if not os.path.exists(dbfile): 337 | for table in self.tables: 338 | sql = table.getCreateStatement() 339 | if sql_debug: 340 | print(sql) 341 | cursor.execute(sql) 342 | self.createInitialData(table) 343 | else: 344 | #如果数据库已经存在。从sqlite_master中读取现存表格的名字。如果有表格尚未存在,就创建它。 345 | #并使用createInitialData()填充基本数据 346 | cursor.execute("select name from sqlite_master where type='table';") 347 | tables = [row[0].lower() for row in cursor.fetchall()] 348 | for table in self.tables: 349 | if table.getName().lower() not in tables: 350 | sql = table.getCreateStatement() 351 | if sql_debug: 352 | print(sql) 353 | cursor.execute(sql) 354 | self.createInitialData(table) 355 | #接下来创建索引。因为创建索引与创建表格不一样,不需要填充基本数据,所以使用if not exists语句。每次 356 | #都用SQL创建一遍。 357 | for table in self.tables: 358 | sql = "create unique index if not exists {pkName}_idx on {tableName} ({pkName});" 359 | sql = sql.format(pkName = table.getPkName(), tableName = table.getName()) 360 | if sql_debug: 361 | print(sql) 362 | cursor.execute(sql) 363 | if not hasattr(table, "indexes"): 364 | continue 365 | for index in table.indexes: 366 | if isinstance(index, str): 367 | sql = "create index if not exists {columnName}_idx on {tableName} ({columnName});" 368 | sql = sql.format(columnName = index, tableName = table.getName()) 369 | elif isinstance(index, (tuple, list)): 370 | sql = "create index if not exists {columnNames1}_idx on {tableName} ({columnNames2});" 371 | columnNames1 = "_".join(index) 372 | columnNames2 = ",".join(index) 373 | sql = sql.format(columnNames1 = columnNames1, columnNames2 = columnNames2, tableName = table.getName()) 374 | if sql_debug: 375 | print(sql) 376 | cursor.execute(sql) 377 | 378 | 379 | def createInitialData(self, table): 380 | "一个虚拟函数,用于创建数据初始值,参数table是表的类型(派生于Table)" 381 | pass 382 | 383 | def conn(self): 384 | if hasattr(transaction_local, "transaction") and \ 385 | transaction_local.transaction: 386 | if transaction_local.conn is None: 387 | transaction_local.conn = sqlite3.connect(self.dbfile, \ 388 | detect_types = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) 389 | transaction_local.conn.row_factory = sqlite3.Row 390 | transaction_local.conn.isolation_level = "DEFERRED" 391 | conn = transaction_local.conn 392 | else: 393 | conn = sqlite3.connect(self.dbfile, \ 394 | detect_types = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) 395 | conn.row_factory = sqlite3.Row 396 | conn.isolation_level = None 397 | return conn 398 | 399 | def __getattr__(self, attrname): 400 | def wrapper(boundMethod, tableName): 401 | def func(*args, **dictArgs): 402 | try: 403 | return boundMethod(tableName, *args, **dictArgs) 404 | except Exception as e: 405 | if __debug__: 406 | traceback.print_exc() 407 | if not isinstance(e, DatabaseException): 408 | raise DatabaseException(e) 409 | else: 410 | raise e 411 | func.__name__ = boundMethod.__name__ 412 | return func 413 | 414 | for method in ["select", "update", "delete", "insert"]: 415 | if attrname.startswith(method): 416 | if method == "select" and attrname.endswith("Ids"): 417 | tableClassName = attrname[len(method): - 3] 418 | method = "selectIds" 419 | else: 420 | tableClassName = attrname[len(method):] 421 | table = self.getTableByClassName(tableClassName) 422 | boundMethod = getattr(self, method) 423 | return wrapper(boundMethod, table.getName()) 424 | raise AttributeError(attrname) 425 | 426 | def extractObject(self, cursor, table): 427 | "从cursor内读取数据对象,返回一列DataObject的list" 428 | resultset = [] 429 | columns = [e[0] for e in cursor.description] 430 | for row in cursor: 431 | record = {} 432 | for column in columns: 433 | #在Python2.6版本中column是bytes类型,并且sqlite3.Row.__getitem__()只接收bytes类型的参数 434 | value = row[column] 435 | if sys.version_info[0] < 3 and isinstance(value, buffer): 436 | value = bytes(value) 437 | record[str(column)] = value 438 | id = record[table.getPkName()] 439 | dataObject = DataObject(id, table, self, record) 440 | resultset.append(dataObject) 441 | return resultset 442 | 443 | def adoptTypes_List(self, parameters): 444 | """很多函数接受list类型的参数。因为sqlite3 for python 2.x需要buffer类型的blob, 445 | 所以这里对参数进行处理。使之兼容2.x与3.x版本。""" 446 | parameters2 = [] 447 | for parameter in parameters: 448 | if sys.version_info[0] < 3 and isinstance(parameter, bytes): 449 | with warnings.catch_warnings(): 450 | warnings.simplefilter("ignore") 451 | parameter = buffer(parameter) 452 | parameters2.append(parameter) 453 | return parameters2 454 | 455 | def adoptTypes_Dict(self, record): 456 | """很多函数接受dict类型的参数。因为sqlite3 for python 2.x需要buffer类型的blob, 457 | 所以这里对参数进行处理。使之兼容2.x与3.x版本。""" 458 | record2 = {} 459 | for field, value in record.items(): 460 | if sys.version_info[0] < 3 and isinstance(value, bytes): 461 | with warnings.catch_warnings(): 462 | warnings.simplefilter("ignore") 463 | value = buffer(value) 464 | record2[field] = value 465 | return record2 466 | 467 | def select(self, tableName, sql, *parameters): 468 | "使用select语句从数据库中取得数据。返回DataObject的列表。" 469 | if sql != "": 470 | sql = "select %s from %s " + sql + ";" 471 | else: 472 | sql = "select %s from %s;" 473 | table = self.getTableBySqlName(tableName) 474 | columns = ",".join(table.getColumnNames()) 475 | parameters = self.adoptTypes_List(parameters) 476 | cursor = self.conn().cursor() 477 | if sql_debug: 478 | print(sql % (columns, tableName), repr(parameters)) 479 | cursor.execute(sql % (columns, tableName), parameters) 480 | return self.extractObject(cursor, table) 481 | 482 | def selectIds(self, tableName, sql, *parameters): 483 | "使用select语句从数据库中取得数据的ID列表。" 484 | if sql != "": 485 | sql = "select %s from %s " + sql + ";" 486 | else: 487 | sql = "select %s from %s;" 488 | table = self.getTableBySqlName(tableName) 489 | parameters = self.adoptTypes_List(parameters) 490 | cursor = self.conn().cursor() 491 | if sql_debug: 492 | print(sql % (table.getPkName(), tableName), repr(parameters)) 493 | cursor.execute(sql % (table.getPkName(), tableName), parameters) 494 | ids = [] 495 | #Python2.6的sqlite3.Row.__getitem__()只接受bytes类型的参数 496 | if sys.version_info[0] < 3: 497 | for row in cursor: 498 | ids.append(row[bytes(table.getPkName())]) 499 | else: 500 | for row in cursor: 501 | ids.append(row[table.getPkName()]) 502 | return ids 503 | 504 | def update(self, tableName, row, sql, *parameters): 505 | "使用update更新数据库表。" 506 | if sql != "": 507 | sql = "update %s set %s " + sql + ";" 508 | else: 509 | sql = "update %s set %s;" 510 | 511 | table = self.getTableBySqlName(tableName) 512 | parameters = self.adoptTypes_List(parameters) 513 | row = self.adoptTypes_Dict(row) 514 | keys = [] 515 | values = [] 516 | for key, value in row.items(): 517 | if key not in table.getColumnNames(): 518 | continue 519 | keys.append(key) 520 | values.append(value) 521 | if len(keys) == 0: 522 | return 523 | values.extend(parameters) 524 | 525 | columns = ",".join([column + "=?" for column in keys]) 526 | conn = self.conn() 527 | cursor = conn.cursor() 528 | if sql_debug: 529 | print(sql % (tableName, columns), repr(values)) 530 | cursor.execute(sql % (tableName, columns), values) 531 | 532 | def delete(self, tableName, sql, *parameters): 533 | "从数据库中删除数据。一般用deleteTableName()的形式调用。" 534 | if sql != "": 535 | sql = "delete from %s " + sql + ";" 536 | else: 537 | sql = "delete from %s;" 538 | parameters = self.adoptTypes_List(parameters) 539 | conn = self.conn() 540 | cursor = conn.cursor() 541 | if sql_debug: 542 | print(sql % tableName, repr(parameters)) 543 | cursor.execute(sql % tableName, parameters) 544 | 545 | def insert(self, tableName, row2): #等下要返回DataObject,所以改名row2 546 | "把数据添加到数据库中。参数是一个dict类型,其中包含了一条纪录。" 547 | table = self.getTableBySqlName(tableName) 548 | sql = "insert into %s (%s) values (%s);" 549 | row = self.adoptTypes_Dict(row2) 550 | keys = [] 551 | values = [] 552 | for key, value in row.items(): 553 | if key not in table.getColumnNames(): 554 | continue 555 | keys.append(key) 556 | values.append(value) 557 | if len(keys) == 0: 558 | return 559 | 560 | columns = ",".join(keys) 561 | questions = ",".join("?" * len(keys)) 562 | conn = self.conn() 563 | cursor = conn.cursor() 564 | if sql_debug: 565 | print(sql % (tableName, columns, questions), repr(values)) 566 | cursor.execute(sql % (tableName, columns, questions), values) 567 | return DataObject(row[table.getPkName()], table, self, row2) 568 | 569 | @classmethod 570 | def getTableBySqlName(cls, tableName): 571 | "根据表格的数据库名返回表格的Python类型。" 572 | for table in cls.tables: 573 | if table.getName() == tableName: 574 | return table 575 | raise InvalidTableException(tableName) 576 | 577 | @classmethod 578 | def getTableByClassName(cls, tableClassName): 579 | "根据表格的类名返回表格的Python类型。" 580 | for table in cls.tables: 581 | if table.__name__ == tableClassName: 582 | return table 583 | raise InvalidTableException(tableClassName) 584 | 585 | class Table: 586 | "用于定义表格的基础类型。" 587 | 588 | @classmethod 589 | def getName(cls): 590 | """返回表名,此处的表名是指在数据库内的表名 591 | 在继承Table的时候可以定义tableName属性。getName()会返回这个属性的值 592 | 如果没有,则使用classNameToSqlName的规则转换类名为表名""" 593 | if hasattr(cls, "tableName"): 594 | return cls.tableName 595 | tableName = classNameToSqlName(cls.__name__) 596 | return tableName 597 | 598 | @classmethod 599 | def getColumnNames(cls): 600 | return cls.columns.keys() 601 | 602 | @classmethod 603 | def getCreateStatement(cls): 604 | tableName = cls.getName() 605 | columnDefination = ",".join([name + " " + type for name, type in cls.columns.items()]) 606 | sql = "create table %s (%s);" % (tableName, columnDefination) 607 | #sql2="create index if not exists %s on %s ()" 608 | #FIXME 为pk增加unique属性 609 | return sql 610 | 611 | @classmethod 612 | def getPkName(cls): 613 | """返回表格的主键名,一般是"id",但是子类也可以定义pkName属性""" 614 | if hasattr(cls, "pkName"): 615 | return cls.pkName 616 | return "id" 617 | 618 | @classmethod 619 | def getIndexes(cls): 620 | "返回表格定义的索引" 621 | return cls.indexes 622 | 623 | 624 | if __name__ == "__main__": 625 | class Person(Table): 626 | pkName = "name" 627 | columns = {"name":"text", "age":"integer", "sex":"text"} 628 | 629 | class MyDatabase(Database): 630 | tables = (Person, ) 631 | 632 | db = MyDatabase("test.dat") 633 | db.insertPerson({"name":"goldfish", "age":25, "sex":"M", "test":"none"}) 634 | db.insertPerson({"name":"kaola", "age":26, "sex":"M", "test":"haha"}) 635 | print(db.selectPerson("")) 636 | db.deletePerson("where name=?", "goldfish") 637 | db.updatePerson({"age":27, "newtest":"okay"}, "where name=?", "kaola") 638 | print(db.selectPerson("")) 639 | 640 | -------------------------------------------------------------------------------- /besteam/utils/winglobalkey.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | from ctypes import windll, Structure, WINFUNCTYPE, POINTER, \ 3 | sizeof, cast, byref 4 | from ctypes.wintypes import HWND, UINT, WPARAM, LPARAM, \ 5 | HINSTANCE, HBRUSH, BOOL, INT, HICON, LPCWSTR, \ 6 | DWORD, LPVOID, HMENU, ATOM, HGDIOBJ 7 | LRESULT = LPARAM 8 | HCURSOR = HICON 9 | NULL = 0 10 | from PyQt5.QtCore import Qt, QObject, pyqtSignal 11 | from PyQt5.QtGui import QKeySequence 12 | 13 | WM_HOTKEY = 0x0312 14 | WM_DESTROY = 0x0002 15 | WS_OVERLAPPEDWINDOW = 0xcf0000 16 | CW_USEDEFAULT = -0x80000000 17 | HWND_DESKTOP = 0 18 | CS_DBLCLKS = 0x8 19 | IDI_APPLICATION = 0x7f00 20 | IDC_ARROW = 0x7f00 21 | WHITE_BRUSH = 0x0 22 | MOD_CONTROL = 0x0002 23 | MOD_ALT = 0x0001 24 | MOD_SHIFT = 0x0004 25 | MOD_WIN = 0x0008 26 | CS_VREDRAW = 1 27 | CS_HREDRAW = 2 28 | WS_EX_OVERLAPPEDWINDOW = 0x300 29 | 30 | WNDPROC = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) 31 | 32 | DefWindowProc = windll.user32.DefWindowProcW 33 | DefWindowProc.argtypes = [HWND, UINT, WPARAM, LPARAM] 34 | DefWindowProc.restype = LRESULT 35 | 36 | CreateWindowEx = windll.user32.CreateWindowExW 37 | CreateWindowEx.argtypes = [DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID] 38 | CreateWindowEx.restype = HWND 39 | 40 | PostMessage = windll.user32.PostMessageW 41 | PostMessage.argtypes = [HWND, UINT, WPARAM, LPARAM] 42 | PostMessage.restype = BOOL 43 | 44 | RegisterHotKey = windll.user32.RegisterHotKey 45 | RegisterHotKey.argtypes = [HWND, INT, UINT, UINT] 46 | RegisterHotKey.restype = BOOL 47 | 48 | UnregisterHotKey = windll.user32.UnregisterHotKey 49 | UnregisterHotKey.argtypes = [HWND, INT] 50 | UnregisterHotKey.restype = BOOL 51 | 52 | class WNDCLASSEX(Structure): 53 | _fields_ = [ 54 | ("cbSize", UINT), 55 | ("style", UINT), 56 | ("lpfnWndProc", WNDPROC), 57 | ("cbClsExtra", INT), 58 | ("cbWndExtra", INT), 59 | ("hInstance", HINSTANCE), 60 | ("hIcon", HICON), 61 | ("hCursor", HCURSOR), 62 | ("hbrBackground", HBRUSH), 63 | ("lpszMenuName", LPCWSTR), 64 | ("lpszClassName", LPCWSTR), 65 | ("hIconSm", HICON), 66 | ] 67 | 68 | RegisterClassEx = windll.user32.RegisterClassExW 69 | RegisterClassEx.argtypes = [POINTER(WNDCLASSEX)] 70 | RegisterClassEx.restype = ATOM 71 | 72 | HOTKEYWIN_CLASSNAME = "HotKeyWin" 73 | 74 | GetModuleHandle = windll.kernel32.GetModuleHandleW 75 | GetModuleHandle.argtypes = [LPCWSTR] 76 | GetModuleHandle.restype = HINSTANCE 77 | 78 | LoadIcon = windll.user32.LoadIconW 79 | LoadIcon.argtypes = [HINSTANCE, LPCWSTR] 80 | LoadIcon.restype = HICON 81 | 82 | LoadCursor = windll.user32.LoadCursorW 83 | LoadCursor.argtypes = [HINSTANCE, LPCWSTR] 84 | LoadCursor.restype = HCURSOR 85 | 86 | UnregisterClass = windll.user32.UnregisterClassW 87 | UnregisterClass.argtypes = [LPCWSTR, HINSTANCE] 88 | UnregisterClass.restype = BOOL 89 | 90 | GetStockObject = windll.gdi32.GetStockObject 91 | GetStockObject.argtypes = [INT] 92 | GetStockObject.restype = HGDIOBJ 93 | 94 | keytable = [ 95 | Qt.Key_unknown, # 0 0x00 96 | Qt.Key_unknown, # 1 0x01 VK_LBUTTON | Left mouse button 97 | Qt.Key_unknown, # 2 0x02 VK_RBUTTON | Right mouse button 98 | Qt.Key_Cancel, # 3 0x03 VK_CANCEL | Control-Break processing 99 | Qt.Key_unknown, # 4 0x04 VK_MBUTTON | Middle mouse button 100 | Qt.Key_unknown, # 5 0x05 VK_XBUTTON1 | X1 mouse button 101 | Qt.Key_unknown, # 6 0x06 VK_XBUTTON2 | X2 mouse button 102 | Qt.Key_unknown, # 7 0x07 -- unassigned -- 103 | Qt.Key_Backspace, # 8 0x08 VK_BACK | BackSpace key 104 | Qt.Key_Tab, # 9 0x09 VK_TAB | Tab key 105 | Qt.Key_unknown, # 10 0x0A -- reserved -- 106 | Qt.Key_unknown, # 11 0x0B -- reserved -- 107 | Qt.Key_Clear, # 12 0x0C VK_CLEAR | Clear key 108 | Qt.Key_Return, # 13 0x0D VK_RETURN | Enter key 109 | Qt.Key_unknown, # 14 0x0E -- unassigned -- 110 | Qt.Key_unknown, # 15 0x0F -- unassigned -- 111 | Qt.Key_Shift, # 16 0x10 VK_SHIFT | Shift key 112 | Qt.Key_Control, # 17 0x11 VK_CONTROL | Ctrl key 113 | Qt.Key_Alt, # 18 0x12 VK_MENU | Alt key 114 | Qt.Key_Pause, # 19 0x13 VK_PAUSE | Pause key 115 | Qt.Key_CapsLock, # 20 0x14 VK_CAPITAL | Caps-Lock 116 | Qt.Key_unknown, # 21 0x15 VK_KANA / VK_HANGUL | IME Kana or Hangul mode 117 | Qt.Key_unknown, # 22 0x16 -- unassigned -- 118 | Qt.Key_unknown, # 23 0x17 VK_JUNJA | IME Junja mode 119 | Qt.Key_unknown, # 24 0x18 VK_FINAL | IME final mode 120 | Qt.Key_unknown, # 25 0x19 VK_HANJA / VK_KANJI | IME Hanja or Kanji mode 121 | Qt.Key_unknown, # 26 0x1A -- unassigned -- 122 | Qt.Key_Escape, # 27 0x1B VK_ESCAPE | Esc key 123 | Qt.Key_unknown, # 28 0x1C VK_CONVERT | IME convert 124 | Qt.Key_unknown, # 29 0x1D VK_NONCONVERT | IME non-convert 125 | Qt.Key_unknown, # 30 0x1E VK_ACCEPT | IME accept 126 | Qt.Key_Mode_switch,# 31 0x1F VK_MODECHANGE | IME mode change request 127 | Qt.Key_Space, # 32 0x20 VK_SPACE | Spacebar 128 | Qt.Key_PageUp, # 33 0x21 VK_PRIOR | Page Up key 129 | Qt.Key_PageDown, # 34 0x22 VK_NEXT | Page Down key 130 | Qt.Key_End, # 35 0x23 VK_END | End key 131 | Qt.Key_Home, # 36 0x24 VK_HOME | Home key 132 | Qt.Key_Left, # 37 0x25 VK_LEFT | Left arrow key 133 | Qt.Key_Up, # 38 0x26 VK_UP | Up arrow key 134 | Qt.Key_Right, # 39 0x27 VK_RIGHT | Right arrow key 135 | Qt.Key_Down, # 40 0x28 VK_DOWN | Down arrow key 136 | Qt.Key_Select, # 41 0x29 VK_SELECT | Select key 137 | Qt.Key_Printer, # 42 0x2A VK_PRINT | Print key 138 | Qt.Key_Execute, # 43 0x2B VK_EXECUTE | Execute key 139 | Qt.Key_Print, # 44 0x2C VK_SNAPSHOT | Print Screen key 140 | Qt.Key_Insert, # 45 0x2D VK_INSERT | Ins key 141 | Qt.Key_Delete, # 46 0x2E VK_DELETE | Del key 142 | Qt.Key_Help, # 47 0x2F VK_HELP | Help key 143 | Qt.Key_0, # 48 0x30 (VK_0) | 0 key 144 | Qt.Key_1, # 49 0x31 (VK_1) | 1 key 145 | Qt.Key_2, # 50 0x32 (VK_2) | 2 key 146 | Qt.Key_3, # 51 0x33 (VK_3) | 3 key 147 | Qt.Key_4, # 52 0x34 (VK_4) | 4 key 148 | Qt.Key_5, # 53 0x35 (VK_5) | 5 key 149 | Qt.Key_6, # 54 0x36 (VK_6) | 6 key 150 | Qt.Key_7, # 55 0x37 (VK_7) | 7 key 151 | Qt.Key_8, # 56 0x38 (VK_8) | 8 key 152 | Qt.Key_9, # 57 0x39 (VK_9) | 9 key 153 | Qt.Key_unknown, # 58 0x3A -- unassigned -- 154 | Qt.Key_unknown, # 59 0x3B -- unassigned -- 155 | Qt.Key_unknown, # 60 0x3C -- unassigned -- 156 | Qt.Key_unknown, # 61 0x3D -- unassigned -- 157 | Qt.Key_unknown, # 62 0x3E -- unassigned -- 158 | Qt.Key_unknown, # 63 0x3F -- unassigned -- 159 | Qt.Key_unknown, # 64 0x40 -- unassigned -- 160 | Qt.Key_A, # 65 0x41 (VK_A) | A key 161 | Qt.Key_B, # 66 0x42 (VK_B) | B key 162 | Qt.Key_C, # 67 0x43 (VK_C) | C key 163 | Qt.Key_D, # 68 0x44 (VK_D) | D key 164 | Qt.Key_E, # 69 0x45 (VK_E) | E key 165 | Qt.Key_F, # 70 0x46 (VK_F) | F key 166 | Qt.Key_G, # 71 0x47 (VK_G) | G key 167 | Qt.Key_H, # 72 0x48 (VK_H) | H key 168 | Qt.Key_I, # 73 0x49 (VK_I) | I key 169 | Qt.Key_J, # 74 0x4A (VK_J) | J key 170 | Qt.Key_K, # 75 0x4B (VK_K) | K key 171 | Qt.Key_L, # 76 0x4C (VK_L) | L key 172 | Qt.Key_M, # 77 0x4D (VK_M) | M key 173 | Qt.Key_N, # 78 0x4E (VK_N) | N key 174 | Qt.Key_O, # 79 0x4F (VK_O) | O key 175 | Qt.Key_P, # 80 0x50 (VK_P) | P key 176 | Qt.Key_Q, # 81 0x51 (VK_Q) | Q key 177 | Qt.Key_R, # 82 0x52 (VK_R) | R key 178 | Qt.Key_S, # 83 0x53 (VK_S) | S key 179 | Qt.Key_T, # 84 0x54 (VK_T) | T key 180 | Qt.Key_U, # 85 0x55 (VK_U) | U key 181 | Qt.Key_V, # 86 0x56 (VK_V) | V key 182 | Qt.Key_W, # 87 0x57 (VK_W) | W key 183 | Qt.Key_X, # 88 0x58 (VK_X) | X key 184 | Qt.Key_Y, # 89 0x59 (VK_Y) | Y key 185 | Qt.Key_Z, # 90 0x5A (VK_Z) | Z key 186 | Qt.Key_Meta, # 91 0x5B VK_LWIN | Left Windows - MS Natural kbd 187 | Qt.Key_Meta, # 92 0x5C VK_RWIN | Right Windows - MS Natural kbd 188 | Qt.Key_Menu, # 93 0x5D VK_APPS | Application key-MS Natural kbd 189 | Qt.Key_unknown, # 94 0x5E -- reserved -- 190 | Qt.Key_Sleep, # 95 0x5F VK_SLEEP 191 | Qt.Key_0, # 96 0x60 VK_NUMPAD0 | Numeric keypad 0 key 192 | Qt.Key_1, # 97 0x61 VK_NUMPAD1 | Numeric keypad 1 key 193 | Qt.Key_2, # 98 0x62 VK_NUMPAD2 | Numeric keypad 2 key 194 | Qt.Key_3, # 99 0x63 VK_NUMPAD3 | Numeric keypad 3 key 195 | Qt.Key_4, # 100 0x64 VK_NUMPAD4 | Numeric keypad 4 key 196 | Qt.Key_5, # 101 0x65 VK_NUMPAD5 | Numeric keypad 5 key 197 | Qt.Key_6, # 102 0x66 VK_NUMPAD6 | Numeric keypad 6 key 198 | Qt.Key_7, # 103 0x67 VK_NUMPAD7 | Numeric keypad 7 key 199 | Qt.Key_8, # 104 0x68 VK_NUMPAD8 | Numeric keypad 8 key 200 | Qt.Key_9, # 105 0x69 VK_NUMPAD9 | Numeric keypad 9 key 201 | Qt.Key_Asterisk, # 106 0x6A VK_MULTIPLY | Multiply key 202 | Qt.Key_Plus, # 107 0x6B VK_ADD | Add key 203 | Qt.Key_Comma, # 108 0x6C VK_SEPARATOR | Separator key 204 | Qt.Key_Minus, # 109 0x6D VK_SUBTRACT | Subtract key 205 | Qt.Key_Period, # 110 0x6E VK_DECIMAL | Decimal key 206 | Qt.Key_Slash, # 111 0x6F VK_DIVIDE | Divide key 207 | Qt.Key_F1, # 112 0x70 VK_F1 | F1 key 208 | Qt.Key_F2, # 113 0x71 VK_F2 | F2 key 209 | Qt.Key_F3, # 114 0x72 VK_F3 | F3 key 210 | Qt.Key_F4, # 115 0x73 VK_F4 | F4 key 211 | Qt.Key_F5, # 116 0x74 VK_F5 | F5 key 212 | Qt.Key_F6, # 117 0x75 VK_F6 | F6 key 213 | Qt.Key_F7, # 118 0x76 VK_F7 | F7 key 214 | Qt.Key_F8, # 119 0x77 VK_F8 | F8 key 215 | Qt.Key_F9, # 120 0x78 VK_F9 | F9 key 216 | Qt.Key_F10, # 121 0x79 VK_F10 | F10 key 217 | Qt.Key_F11, # 122 0x7A VK_F11 | F11 key 218 | Qt.Key_F12, # 123 0x7B VK_F12 | F12 key 219 | Qt.Key_F13, # 124 0x7C VK_F13 | F13 key 220 | Qt.Key_F14, # 125 0x7D VK_F14 | F14 key 221 | Qt.Key_F15, # 126 0x7E VK_F15 | F15 key 222 | Qt.Key_F16, # 127 0x7F VK_F16 | F16 key 223 | Qt.Key_F17, # 128 0x80 VK_F17 | F17 key 224 | Qt.Key_F18, # 129 0x81 VK_F18 | F18 key 225 | Qt.Key_F19, # 130 0x82 VK_F19 | F19 key 226 | Qt.Key_F20, # 131 0x83 VK_F20 | F20 key 227 | Qt.Key_F21, # 132 0x84 VK_F21 | F21 key 228 | Qt.Key_F22, # 133 0x85 VK_F22 | F22 key 229 | Qt.Key_F23, # 134 0x86 VK_F23 | F23 key 230 | Qt.Key_F24, # 135 0x87 VK_F24 | F24 key 231 | Qt.Key_unknown, # 136 0x88 -- unassigned -- 232 | Qt.Key_unknown, # 137 0x89 -- unassigned -- 233 | Qt.Key_unknown, # 138 0x8A -- unassigned -- 234 | Qt.Key_unknown, # 139 0x8B -- unassigned -- 235 | Qt.Key_unknown, # 140 0x8C -- unassigned -- 236 | Qt.Key_unknown, # 141 0x8D -- unassigned -- 237 | Qt.Key_unknown, # 142 0x8E -- unassigned -- 238 | Qt.Key_unknown, # 143 0x8F -- unassigned -- 239 | Qt.Key_NumLock, # 144 0x90 VK_NUMLOCK | Num Lock key 240 | Qt.Key_ScrollLock, # 145 0x91 VK_SCROLL | Scroll Lock key 241 | # Fujitsu/OASYS kbd -------------------- 242 | 0, #Qt.Key_Jisho, # 146 0x92 VK_OEM_FJ_JISHO | 'Dictionary' key / 243 | # VK_OEM_NEC_EQUAL = key on numpad on NEC PC-9800 kbd 244 | Qt.Key_Massyo, # 147 0x93 VK_OEM_FJ_MASSHOU | 'Unregister word' key 245 | Qt.Key_Touroku, # 148 0x94 VK_OEM_FJ_TOUROKU | 'Register word' key 246 | 0, #Qt.Key_Oyayubi_Left,#149 0x95 VK_OEM_FJ_LOYA | 'Left OYAYUBI' key 247 | 0, #Qt.Key_Oyayubi_Right,#150 0x96 VK_OEM_FJ_ROYA | 'Right OYAYUBI' key 248 | Qt.Key_unknown, # 151 0x97 -- unassigned -- 249 | Qt.Key_unknown, # 152 0x98 -- unassigned -- 250 | Qt.Key_unknown, # 153 0x99 -- unassigned -- 251 | Qt.Key_unknown, # 154 0x9A -- unassigned -- 252 | Qt.Key_unknown, # 155 0x9B -- unassigned -- 253 | Qt.Key_unknown, # 156 0x9C -- unassigned -- 254 | Qt.Key_unknown, # 157 0x9D -- unassigned -- 255 | Qt.Key_unknown, # 158 0x9E -- unassigned -- 256 | Qt.Key_unknown, # 159 0x9F -- unassigned -- 257 | Qt.Key_Shift, # 160 0xA0 VK_LSHIFT | Left Shift key 258 | Qt.Key_Shift, # 161 0xA1 VK_RSHIFT | Right Shift key 259 | Qt.Key_Control, # 162 0xA2 VK_LCONTROL | Left Ctrl key 260 | Qt.Key_Control, # 163 0xA3 VK_RCONTROL | Right Ctrl key 261 | Qt.Key_Alt, # 164 0xA4 VK_LMENU | Left Menu key 262 | Qt.Key_Alt, # 165 0xA5 VK_RMENU | Right Menu key 263 | Qt.Key_Back, # 166 0xA6 VK_BROWSER_BACK | Browser Back key 264 | Qt.Key_Forward, # 167 0xA7 VK_BROWSER_FORWARD | Browser Forward key 265 | Qt.Key_Refresh, # 168 0xA8 VK_BROWSER_REFRESH | Browser Refresh key 266 | Qt.Key_Stop, # 169 0xA9 VK_BROWSER_STOP | Browser Stop key 267 | Qt.Key_Search, # 170 0xAA VK_BROWSER_SEARCH | Browser Search key 268 | Qt.Key_Favorites, # 171 0xAB VK_BROWSER_FAVORITES| Browser Favorites key 269 | Qt.Key_HomePage, # 172 0xAC VK_BROWSER_HOME | Browser Start and Home key 270 | Qt.Key_VolumeMute, # 173 0xAD VK_VOLUME_MUTE | Volume Mute key 271 | Qt.Key_VolumeDown, # 174 0xAE VK_VOLUME_DOWN | Volume Down key 272 | Qt.Key_VolumeUp, # 175 0xAF VK_VOLUME_UP | Volume Up key 273 | Qt.Key_MediaNext, # 176 0xB0 VK_MEDIA_NEXT_TRACK | Next Track key 274 | Qt.Key_MediaPrevious, #177 0xB1 VK_MEDIA_PREV_TRACK | Previous Track key 275 | Qt.Key_MediaStop, # 178 0xB2 VK_MEDIA_STOP | Stop Media key 276 | Qt.Key_MediaPlay, # 179 0xB3 VK_MEDIA_PLAY_PAUSE | Play/Pause Media key 277 | Qt.Key_LaunchMail, # 180 0xB4 VK_LAUNCH_MAIL | Start Mail key 278 | Qt.Key_LaunchMedia,# 181 0xB5 VK_LAUNCH_MEDIA_SELECT Select Media key 279 | Qt.Key_Launch0, # 182 0xB6 VK_LAUNCH_APP1 | Start Application 1 key 280 | Qt.Key_Launch1, # 183 0xB7 VK_LAUNCH_APP2 | Start Application 2 key 281 | Qt.Key_unknown, # 184 0xB8 -- reserved -- 282 | Qt.Key_unknown, # 185 0xB9 -- reserved -- 283 | Qt.Key_Semicolon, # 186 0xBA VK_OEM_1 | ';:' for US 284 | Qt.Key_Plus, # 187 0xBB VK_OEM_PLUS | '+' any country 285 | Qt.Key_Comma, # 188 0xBC VK_OEM_COMMA | ',' any country 286 | Qt.Key_Minus, # 189 0xBD VK_OEM_MINUS | '-' any country 287 | Qt.Key_Period, # 190 0xBE VK_OEM_PERIOD | '.' any country 288 | Qt.Key_Slash, # 191 0xBF VK_OEM_2 | '/?' for US 289 | Qt.Key_QuoteLeft, # 192 0xC0 VK_OEM_3 | '`~' for US 290 | Qt.Key_unknown, # 193 0xC1 -- reserved -- 291 | Qt.Key_unknown, # 194 0xC2 -- reserved -- 292 | Qt.Key_unknown, # 195 0xC3 -- reserved -- 293 | Qt.Key_unknown, # 196 0xC4 -- reserved -- 294 | Qt.Key_unknown, # 197 0xC5 -- reserved -- 295 | Qt.Key_unknown, # 198 0xC6 -- reserved -- 296 | Qt.Key_unknown, # 199 0xC7 -- reserved -- 297 | Qt.Key_unknown, # 200 0xC8 -- reserved -- 298 | Qt.Key_unknown, # 201 0xC9 -- reserved -- 299 | Qt.Key_unknown, # 202 0xCA -- reserved -- 300 | Qt.Key_unknown, # 203 0xCB -- reserved -- 301 | Qt.Key_unknown, # 204 0xCC -- reserved -- 302 | Qt.Key_unknown, # 205 0xCD -- reserved -- 303 | Qt.Key_unknown, # 206 0xCE -- reserved -- 304 | Qt.Key_unknown, # 207 0xCF -- reserved -- 305 | Qt.Key_unknown, # 208 0xD0 -- reserved -- 306 | Qt.Key_unknown, # 209 0xD1 -- reserved -- 307 | Qt.Key_unknown, # 210 0xD2 -- reserved -- 308 | Qt.Key_unknown, # 211 0xD3 -- reserved -- 309 | Qt.Key_unknown, # 212 0xD4 -- reserved -- 310 | Qt.Key_unknown, # 213 0xD5 -- reserved -- 311 | Qt.Key_unknown, # 214 0xD6 -- reserved -- 312 | Qt.Key_unknown, # 215 0xD7 -- reserved -- 313 | Qt.Key_unknown, # 216 0xD8 -- unassigned -- 314 | Qt.Key_unknown, # 217 0xD9 -- unassigned -- 315 | Qt.Key_unknown, # 218 0xDA -- unassigned -- 316 | Qt.Key_BraceLeft, # 219 0xDB VK_OEM_4 | '[{' for US 317 | 0, # 220 0xDC VK_OEM_5 | '\|' for US 318 | Qt.Key_BraceRight, # 221 0xDD VK_OEM_6 | ']}' for US 319 | Qt.Key_Apostrophe, # 222 0xDE VK_OEM_7 | ''"' for US 320 | 0, # 223 0xDF VK_OEM_8 321 | Qt.Key_unknown, # 224 0xE0 -- reserved -- 322 | Qt.Key_unknown, # 225 0xE1 VK_OEM_AX | 'AX' key on Japanese AX kbd 323 | Qt.Key_unknown, # 226 0xE2 VK_OEM_102 | "<>" or "\|" on RT 102-key kbd 324 | Qt.Key_unknown, # 227 0xE3 VK_ICO_HELP | Help key on ICO 325 | Qt.Key_unknown, # 228 0xE4 VK_ICO_00 | 00 key on ICO 326 | Qt.Key_unknown, # 229 0xE5 VK_PROCESSKEY | IME Process key 327 | Qt.Key_unknown, # 230 0xE6 VK_ICO_CLEAR | 328 | Qt.Key_unknown, # 231 0xE7 VK_PACKET | Unicode char as keystrokes 329 | Qt.Key_unknown, # 232 0xE8 -- unassigned -- 330 | # Nokia/Ericsson definitions --------------- 331 | Qt.Key_unknown, # 233 0xE9 VK_OEM_RESET 332 | Qt.Key_unknown, # 234 0xEA VK_OEM_JUMP 333 | Qt.Key_unknown, # 235 0xEB VK_OEM_PA1 334 | Qt.Key_unknown, # 236 0xEC VK_OEM_PA2 335 | Qt.Key_unknown, # 237 0xED VK_OEM_PA3 336 | Qt.Key_unknown, # 238 0xEE VK_OEM_WSCTRL 337 | Qt.Key_unknown, # 239 0xEF VK_OEM_CUSEL 338 | Qt.Key_unknown, # 240 0xF0 VK_OEM_ATTN 339 | Qt.Key_unknown, # 241 0xF1 VK_OEM_FINISH 340 | Qt.Key_unknown, # 242 0xF2 VK_OEM_COPY 341 | Qt.Key_unknown, # 243 0xF3 VK_OEM_AUTO 342 | Qt.Key_unknown, # 244 0xF4 VK_OEM_ENLW 343 | Qt.Key_unknown, # 245 0xF5 VK_OEM_BACKTAB 344 | Qt.Key_unknown, # 246 0xF6 VK_ATTN | Attn key 345 | Qt.Key_unknown, # 247 0xF7 VK_CRSEL | CrSel key 346 | Qt.Key_unknown, # 248 0xF8 VK_EXSEL | ExSel key 347 | Qt.Key_unknown, # 249 0xF9 VK_EREOF | Erase EOF key 348 | Qt.Key_Play, # 250 0xFA VK_PLAY | Play key 349 | Qt.Key_Zoom, # 251 0xFB VK_ZOOM | Zoom key 350 | Qt.Key_unknown, # 252 0xFC VK_NONAME | Reserved 351 | Qt.Key_unknown, # 253 0xFD VK_PA1 | PA1 key 352 | Qt.Key_Clear, # 254 0xFE VK_OEM_CLEAR | Clear key 353 | ] 354 | 355 | 356 | inited = False 357 | 358 | def HotKeyWinProc(hwnd, message, wParam, lParam): 359 | if message == WM_HOTKEY: 360 | raw_winkey = (lParam & 0xFFFF0000) >> 16 361 | modifier = lParam & 0xFFFF 362 | try: 363 | notify(hwnd, modifier, raw_winkey) 364 | except: 365 | pass 366 | return 0 367 | return DefWindowProc(hwnd, message, wParam, lParam) 368 | HotKeyWinProc = WNDPROC(HotKeyWinProc) 369 | 370 | def createHotKeyWindow(): 371 | if not inited: 372 | return None 373 | hwnd = CreateWindowEx( 374 | WS_EX_OVERLAPPEDWINDOW,#Extended possibilites for variation 375 | HOTKEYWIN_CLASSNAME, #Classname 376 | HOTKEYWIN_CLASSNAME, #Title Text 377 | WS_OVERLAPPEDWINDOW, #default window style 378 | CW_USEDEFAULT, #Windows decides the position 379 | CW_USEDEFAULT, #where the window ends up on the screen 380 | CW_USEDEFAULT, #The programs width 381 | CW_USEDEFAULT, #and height in pixels 382 | HWND_DESKTOP, #The window is a child-window to desktop 383 | NULL, #No menu 384 | GetModuleHandle(cast(NULL, LPCWSTR)), #Program Instance handler 385 | NULL #No Window Creation data 386 | ) 387 | return hwnd 388 | 389 | def registerWindowClass(): 390 | global inited 391 | if inited: 392 | return True 393 | wincl = WNDCLASSEX() 394 | wincl.hInstance = GetModuleHandle(cast(NULL, LPCWSTR)) 395 | wincl.lpszClassName = HOTKEYWIN_CLASSNAME 396 | wincl.lpfnWndProc = HotKeyWinProc # This function is called by windows 397 | wincl.style = CS_DBLCLKS | CS_VREDRAW | CS_HREDRAW # Catch double-clicks 398 | wincl.cbSize = sizeof(WNDCLASSEX) 399 | wincl.hIcon = LoadIcon(NULL, cast(IDI_APPLICATION, LPCWSTR)) 400 | wincl.hIconSm = NULL 401 | wincl.hCursor = LoadCursor(NULL, cast(IDC_ARROW, LPCWSTR)) 402 | wincl.lpszMenuName = NULL #No menu 403 | wincl.cbClsExtra = 0 #No extra bytes after the window class 404 | wincl.cbWndExtra = 0 #structure or the window instance 405 | #Use Windows's default color as the background of the window 406 | wincl.hbrBackground = GetStockObject(WHITE_BRUSH) 407 | if RegisterClassEx(byref(wincl)): 408 | inited = True 409 | return inited 410 | 411 | def notify(hwnd, modifier, raw_winkey): 412 | ref = GlobalKey.instances[hwnd] 413 | if not ref(): 414 | return 415 | for keyId, (qtkey, winkey) in ref().keys.items(): 416 | modifier_, raw_winkey_ = winkey 417 | if modifier_ == modifier and raw_winkey_ == raw_winkey: 418 | ref().catched.emit(keyId) 419 | 420 | def translateKey(qtkey): 421 | key = qtkey[0] 422 | raw_qtkey = key & ~(Qt.CTRL | Qt.ALT | Qt.SHIFT |Qt.META) 423 | try: 424 | raw_winkey = keytable.index(raw_qtkey) 425 | except IndexError: 426 | raw_winkey = 0 427 | modifier = 0 428 | if key & Qt.CTRL: 429 | modifier |= MOD_CONTROL 430 | if key & Qt.ALT: 431 | modifier |= MOD_ALT 432 | if key & Qt.SHIFT: 433 | modifier |= MOD_SHIFT 434 | if key & Qt.META: 435 | modifier |= MOD_WIN 436 | return modifier, raw_winkey 437 | 438 | class GlobalKey(QObject): 439 | catched = pyqtSignal(int) 440 | instances = {} 441 | 442 | def __init__(self): 443 | super(GlobalKey, self).__init__() 444 | registerWindowClass() 445 | self.hwnd = createHotKeyWindow() 446 | self.nextId = 0 447 | self.keys = {} # keyId : (qtkey, (modifier, raw_winkey)) 448 | if self.hwnd: 449 | GlobalKey.instances[self.hwnd] = weakref.ref(self) 450 | 451 | def close(self): 452 | if not self.hwnd: 453 | return 454 | for keyId in self.keys.keys(): 455 | UnregisterHotKey(self.hwnd, keyId) 456 | PostMessage(self.hwnd, WM_DESTROY, 0, 0) 457 | del GlobalKey.instances[self.hwnd] 458 | 459 | def addHotKey(self, name, qtkey): 460 | qtkey = QKeySequence(qtkey) 461 | if not self.hwnd: 462 | return -1 463 | if qtkey in self.keys.values(): 464 | return -1 465 | modifier, raw_winkey = translateKey(qtkey) 466 | if not RegisterHotKey(self.hwnd, self.nextId, modifier, raw_winkey): 467 | return -1 468 | self.keys[self.nextId] = (qtkey, (modifier, raw_winkey)) 469 | self.nextId += 1 470 | return self.nextId - 1 471 | 472 | def removeHotKey(self, keyId): 473 | if keyId in self.keys: 474 | UnregisterHotKey(self.hwnd, keyId) 475 | del self.keys[keyId] 476 | 477 | if __name__ == "__main__": 478 | from PyQt4.QtGui import QApplication, QPlainTextEdit 479 | 480 | app = QApplication([]) 481 | w = QPlainTextEdit() 482 | w.show() 483 | globalKey = GlobalKey() 484 | globalKey.addHotKey("Test", QKeySequence("Ctrl+`")) 485 | def gotKey(keyId): 486 | w.appendPlainText("Got Key") 487 | #globalKey.removeHotKey(keyId) 488 | #globalKey.close() 489 | globalKey.catched.connect(gotKey) 490 | app.exec_() 491 | 492 | -------------------------------------------------------------------------------- /clean.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | current_dir = os.path.dirname(os.path.abspath(__file__)) 6 | os.unlink(os.path.join(current_dir, "quickpanel_rc.py")) 7 | for root, dirs, filenames in os.walk(current_dir): 8 | for filename in filenames: 9 | path = os.path.join(root, filename) 10 | if filename.endswith((".pyc", ".pyo")): 11 | os.unlink(path) 12 | elif filename.startswith("Ui_") and filename.endswith(".py"): 13 | os.unlink(path) 14 | -------------------------------------------------------------------------------- /compile_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | 6 | class NotFound(Exception): 7 | pass 8 | 9 | def findPyQtTools_win32(): 10 | pythondir = os.path.dirname(sys.executable) 11 | systempaths = os.environ["PATH"].split(";") 12 | systempaths = [path.strip() for path in systempaths] 13 | 14 | def findTool(toolname): 15 | tool = os.path.join(pythondir, toolname) 16 | if not os.path.exists(tool): 17 | tool = os.path.join(pythondir, "Scripts", toolname) 18 | if not os.path.exists(tool): 19 | tool = os.path.join(pythondir, "Lib\\site-packages\\PyQt5", toolname) 20 | if not os.path.exists(tool): 21 | for systempath in systempaths: 22 | tool = os.path.join(systempath, toolname) 23 | if os.path.exists(tool): 24 | break 25 | if not os.path.exists(tool): 26 | raise NotFound() 27 | return tool 28 | 29 | pyrcc = findTool("pyrcc5.bat") 30 | pyuic = findTool("pyuic5.bat") 31 | return pyrcc, pyuic 32 | 33 | def findPyQtTools_linux(): 34 | pyrcc = "/usr/bin/pyrcc5" 35 | if not os.path.exists(pyrcc): 36 | pyrcc = "/usr/local/bin/pyrcc5" 37 | if not os.path.exists(pyrcc): 38 | raise NotFound() 39 | 40 | pyuic = "/usr/bin/pyuic5" 41 | if not os.path.exists(pyuic): 42 | pyuic = "/usr/local/bin/pyuic5" 43 | if not os.path.exists(pyuic): 44 | raise NotFound() 45 | 46 | return pyrcc, pyuic 47 | 48 | curdir = os.path.abspath(os.path.dirname(__file__)) 49 | J = os.path.join 50 | 51 | try: 52 | pyrcc, pyuic = findPyQtTools_win32() if os.name == "nt" else findPyQtTools_linux() 53 | except NotFound: 54 | print("pyqt tools are not found in your machine.") 55 | sys.exit(1) 56 | 57 | if not os.path.exists(J(curdir, "quickpanel_rc.py")) or \ 58 | os.path.getmtime(J(curdir, "quickpanel_rc.py")) < os.path.getmtime(J(curdir, "quickpanel.qrc")): 59 | print("compile quickpanel.qrc to quickpanel_rc.py") 60 | os.system("{2} -o {0} {1}".format(J(curdir, "quickpanel_rc.py"), J(curdir, "quickpanel.qrc"), pyrcc)) 61 | 62 | for root, dirs, files in os.walk(os.path.join(curdir, "besteam")): 63 | for filename in files: 64 | if not filename.endswith(".ui"): 65 | continue 66 | basename = "Ui_" + os.path.splitext(filename)[0] + ".py" 67 | pyfile = J(root, basename) 68 | uifile = J(root, filename) 69 | if os.path.exists(pyfile) and os.path.getmtime(pyfile) > os.path.getmtime(uifile): 70 | continue 71 | print("compiling", uifile, "to", pyfile) 72 | os.system("{2} -x -o {0} {1}".format(pyfile, uifile, pyuic)) 73 | -------------------------------------------------------------------------------- /images/angelfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/angelfish.png -------------------------------------------------------------------------------- /images/change_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/change_background.png -------------------------------------------------------------------------------- /images/change_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/change_layout.png -------------------------------------------------------------------------------- /images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/close.png -------------------------------------------------------------------------------- /images/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/configure.png -------------------------------------------------------------------------------- /images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/delete.png -------------------------------------------------------------------------------- /images/edit-rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/edit-rename.png -------------------------------------------------------------------------------- /images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/edit.png -------------------------------------------------------------------------------- /images/folder-documents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/folder-documents.png -------------------------------------------------------------------------------- /images/folder-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/folder-image.png -------------------------------------------------------------------------------- /images/folder-sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/folder-sound.png -------------------------------------------------------------------------------- /images/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/hello.png -------------------------------------------------------------------------------- /images/httpurl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/httpurl.png -------------------------------------------------------------------------------- /images/insert-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/insert-link.png -------------------------------------------------------------------------------- /images/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/new.png -------------------------------------------------------------------------------- /images/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/reset.png -------------------------------------------------------------------------------- /images/select_widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/select_widgets.png -------------------------------------------------------------------------------- /images/tetrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/tetrix.png -------------------------------------------------------------------------------- /images/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/unknown.png -------------------------------------------------------------------------------- /images/user-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/user-home.png -------------------------------------------------------------------------------- /images/weather/weather-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-clear.png -------------------------------------------------------------------------------- /images/weather/weather-clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-clouds.png -------------------------------------------------------------------------------- /images/weather/weather-freezing-rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-freezing-rain.png -------------------------------------------------------------------------------- /images/weather/weather-many-clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-many-clouds.png -------------------------------------------------------------------------------- /images/weather/weather-none-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-none-available.png -------------------------------------------------------------------------------- /images/weather/weather-showers-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-showers-day.png -------------------------------------------------------------------------------- /images/weather/weather-showers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-showers.png -------------------------------------------------------------------------------- /images/weather/weather-snow-rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-snow-rain.png -------------------------------------------------------------------------------- /images/weather/weather-snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-snow.png -------------------------------------------------------------------------------- /images/weather/weather-storm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgoldfish/quickpanel/e4bc4b450fc77e31fee26a8106b1b87866ef5bf2/images/weather/weather-storm.png -------------------------------------------------------------------------------- /quickpanel.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/angelfish.png 4 | images/change_background.png 5 | images/change_layout.png 6 | images/close.png 7 | images/configure.png 8 | images/reset.png 9 | images/select_widgets.png 10 | images/tetrix.png 11 | images/hello.png 12 | images/user-home.png 13 | images/folder-documents.png 14 | images/folder-image.png 15 | images/folder-sound.png 16 | images/httpurl.png 17 | images/unknown.png 18 | images/new.png 19 | images/edit.png 20 | images/delete.png 21 | images/edit-rename.png 22 | images/insert-link.png 23 | images/weather/weather-clear.png 24 | images/weather/weather-clouds.png 25 | images/weather/weather-freezing-rain.png 26 | images/weather/weather-many-clouds.png 27 | images/weather/weather-none-available.png 28 | images/weather/weather-showers.png 29 | images/weather/weather-showers-day.png 30 | images/weather/weather-snow.png 31 | images/weather/weather-snow-rain.png 32 | images/weather/weather-storm.png 33 | 34 | -------------------------------------------------------------------------------- /start_quickpanel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import logging 4 | import os 5 | from PyQt5.QtCore import QObject, pyqtSignal, QStandardPaths 6 | from PyQt5.QtGui import QFont, QIcon, QKeySequence 7 | from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QPushButton, QLabel, QDialog, QSizePolicy, \ 8 | QDialogButtonBox, QVBoxLayout, QHBoxLayout, QAction, QMessageBox 9 | import quickpanel_rc; quickpanel_rc 10 | from besteam.utils.settings import Settings, _Settings 11 | from besteam.utils.globalkey import GlobalKey 12 | from besteam.im.quick_panel import QuickPanel 13 | from tetrix import TetrixWindow 14 | 15 | logger = logging.getLogger("quickpanel") 16 | 17 | class SetKeyWidget(QPushButton): 18 | editingFinished = pyqtSignal() 19 | 20 | def __init__(self, parent = None): 21 | QPushButton.__init__(self, parent) 22 | self.setCheckable(True) 23 | 24 | def keyPressEvent(self, event): 25 | if self.isChecked() and event.text() != "": 26 | self.setText(QKeySequence(event.key() | int(event.modifiers())).toString(QKeySequence.NativeText)) 27 | self.setChecked(False) 28 | self.editingFinished.emit() 29 | else: 30 | QPushButton.keyPressEvent(self, event) 31 | 32 | class ConfigureDialog(QDialog): 33 | def __init__(self): 34 | QDialog.__init__(self) 35 | self.lblKey = QLabel("Global Key:") 36 | self.btnKey = SetKeyWidget(self) 37 | self.btnKey.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 38 | buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 39 | self.setLayout(QVBoxLayout()) 40 | layout1 = QHBoxLayout() 41 | layout1.addWidget(self.lblKey) 42 | layout1.addWidget(self.btnKey) 43 | self.layout().addLayout(layout1) 44 | self.layout().addStretch() 45 | self.layout().addWidget(buttonBox) 46 | buttonBox.accepted.connect(self.accept) 47 | buttonBox.rejected.connect(self.reject) 48 | 49 | def setGlobalKey(self, globalKeyName): 50 | self.btnKey.setText(globalKeyName) 51 | 52 | def getGlobalKey(self): 53 | return self.btnKey.text() 54 | 55 | 56 | class Platform(QObject): 57 | def __init__(self): 58 | QObject.__init__(self) 59 | documentsLocation = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) 60 | self.databaseFile = os.path.join(documentsLocation, "quickpanel.db") 61 | self._settings = _Settings(self.databaseFile) 62 | self.globalKey = GlobalKey() 63 | self.quickPanel = QuickPanel(self) 64 | self.actionConfigure = QAction(QIcon(":/images/configure.png"), \ 65 | self.tr("&Configure"), self) 66 | self.actionExit = QAction(QIcon(":/images/close.png"), \ 67 | self.tr("&Exit"), self) 68 | self.trayIcon = QSystemTrayIcon(QIcon(":/images/angelfish.png")) 69 | self.contextMenu = QMenu() 70 | self.contextMenu.addAction(self.actionConfigure) 71 | self.contextMenu.addAction(self.actionExit) 72 | self.trayIcon.setContextMenu(self.contextMenu) 73 | self.actionConfigure.triggered.connect(self.configure) 74 | self.actionExit.triggered.connect(self.exit) 75 | self.trayIcon.activated.connect(self.onTrayIconActivated) 76 | 77 | def start(self): 78 | self.loadSettings() 79 | self.trayIcon.show() 80 | self.quickPanel.initWidgets() 81 | logger.info("QuickPanel is launched.") 82 | self.quickPanel.addQuickAccessShortcut(self.tr("Tetrix"), \ 83 | QIcon(":/images/tetrix.png"), self.startTetrix) 84 | self.quickPanel.addQuickAccessShortcut(self.tr("Hello"), \ 85 | QIcon(":/images/hello.png"), self.sayHello) 86 | 87 | def startTetrix(self): 88 | if getattr(self, "tetrixWindow", None) is None: 89 | self.tetrixWindow = TetrixWindow() 90 | self.tetrixWindow.show() 91 | self.tetrixWindow.activateWindow() 92 | 93 | def sayHello(self): 94 | QMessageBox.information(None, self.tr("Say Hello."), \ 95 | self.tr("Hello, world!")) 96 | 97 | def loadSettings(self): 98 | settings = self.getSettings() 99 | keyname = settings.value("globalkey", "Alt+`") 100 | self.keyId = self.globalKey.addHotKey("actionToggleQuickPanel", keyname) 101 | self.globalKey.catched.connect(self.quickPanel.toggle) 102 | 103 | def saveSettings(self): 104 | pass 105 | 106 | def configure(self): 107 | d = ConfigureDialog() 108 | settings = self.getSettings() 109 | keyname = settings.value("globalkey", "Alt+`") 110 | d.setGlobalKey(keyname) 111 | self.globalKey.removeHotKey(self.keyId) 112 | try: 113 | result = getattr(d, "exec")() 114 | except AttributeError: 115 | result = getattr(d, "exec_")() 116 | if result == QDialog.Accepted: 117 | keyname = d.getGlobalKey() 118 | settings.setValue("globalkey", keyname) 119 | self.keyId = self.globalKey.addHotKey("actionToggleQuickPanel", keyname) 120 | 121 | def exit(self): 122 | self.quickPanel.finalize() 123 | self.saveSettings() 124 | self.globalKey.close() 125 | logger.info("QuickPanel is shutting down.") 126 | QApplication.instance().quit() 127 | 128 | def onTrayIconActivated(self, reason): 129 | if reason == QSystemTrayIcon.Trigger: 130 | self.quickPanel.toggle() 131 | 132 | def getSettings(self): 133 | return Settings(self._settings) 134 | 135 | def main(): 136 | app = QApplication(sys.argv) 137 | if sys.platform == "win32": 138 | app.setFont(QFont("Tahoma", 9)) 139 | app.setOrganizationName("Besteam") 140 | app.setOrganizationDomain("besteam.im") 141 | app.setApplicationName("QuickPanel") 142 | app.setQuitOnLastWindowClosed(False) 143 | app.setWindowIcon(QIcon(":/images/angelfish.png")) 144 | #app.setStyle(QStyleFactory.create("qtcurve")) 145 | platform = Platform() 146 | platform.start() 147 | try: 148 | getattr(app, "exec")() 149 | except AttributeError: 150 | getattr(app, "exec_")() 151 | 152 | if __name__ == "__main__": 153 | logging.basicConfig(level = logging.DEBUG) 154 | main() 155 | -------------------------------------------------------------------------------- /tetrix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import random 4 | import sys 5 | import sip 6 | 7 | from PyQt5.QtCore import Qt, QBasicTimer, pyqtSignal, QSize 8 | from PyQt5.QtGui import QPainter, QPixmap, QPalette, QColor, QBrush 9 | from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QFrame, QLCDNumber, QSizePolicy, QPushButton, QHBoxLayout, QVBoxLayout 10 | 11 | 12 | NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape = range(8) 13 | 14 | random.seed(None) 15 | 16 | __all__ = ["TetrixWindow"] 17 | 18 | class TetrixWindow(QWidget): 19 | def __init__(self, parent = None): 20 | QWidget.__init__(self, parent, Qt.Window) 21 | 22 | self.board = TetrixBoard() 23 | self.indictor = TetrixIndictor() 24 | 25 | nextPieceLabel = QLabel(self) 26 | nextPieceLabel.setFrameStyle(QFrame.Box | QFrame.Raised) 27 | nextPieceLabel.setAlignment(Qt.AlignCenter) 28 | self.board.setNextPieceLabel(nextPieceLabel) 29 | 30 | scoreLcd = QLCDNumber(6) 31 | scoreLcd.setSegmentStyle(QLCDNumber.Filled) 32 | levelLcd = QLCDNumber(2) 33 | levelLcd.setSegmentStyle(QLCDNumber.Filled) 34 | linesLcd = QLCDNumber(6) 35 | linesLcd.setSegmentStyle(QLCDNumber.Filled) 36 | 37 | startButton = QPushButton(self.tr("&Start")) 38 | startButton.setFocusPolicy(Qt.NoFocus) 39 | quitButton = QPushButton(self.tr("E&xit")) 40 | quitButton.setFocusPolicy(Qt.NoFocus) 41 | pauseButton = QPushButton(self.tr("&Pause")) 42 | pauseButton.setFocusPolicy(Qt.NoFocus) 43 | 44 | startButton.clicked.connect(self.board.start) 45 | pauseButton.clicked.connect(self.board.pause) 46 | quitButton.clicked.connect(self.close) 47 | self.board.scoreChanged.connect(scoreLcd.display) 48 | self.board.levelChanged.connect(levelLcd.display) 49 | self.board.linesRemovedChanged.connect(linesLcd.display) 50 | self.board.act.connect(self.indictor.showIndictor) 51 | 52 | layout1 = QHBoxLayout() 53 | layout3 = QVBoxLayout() 54 | layout3.addWidget(self.board) 55 | layout3.addWidget(self.indictor) 56 | layout3.setSpacing(0) 57 | layout1.addLayout(layout3) 58 | layout2 = QVBoxLayout() 59 | layout2.addWidget(self.createLabel(self.tr("Next Block"))) 60 | layout2.addWidget(nextPieceLabel) 61 | layout2.addWidget(self.createLabel(self.tr("Level"))) 62 | layout2.addWidget(levelLcd) 63 | layout2.addWidget(self.createLabel(self.tr("Score")),) 64 | layout2.addWidget(scoreLcd) 65 | layout2.addWidget(self.createLabel(self.tr("Total Lines"))) 66 | layout2.addWidget(linesLcd) 67 | layout2.addWidget(startButton) 68 | layout2.addWidget(quitButton) 69 | layout2.addWidget(pauseButton) 70 | layout1.addLayout(layout2) 71 | layout1.setStretch(0, 75) 72 | layout1.setStretch(1, 25) 73 | self.setLayout(layout1) 74 | 75 | self.setWindowTitle(self.tr("Tetrix")) 76 | self.resize(self.logicalDpiX() / 96 * 275, self.logicalDpiY() / 96 * 380) 77 | 78 | r = self.geometry() 79 | r.moveCenter(QApplication.instance().desktop().screenGeometry().center()) 80 | self.setGeometry(r) 81 | 82 | def createLabel(self, text): 83 | lbl = QLabel(text) 84 | lbl.setAlignment(Qt.AlignHCenter | Qt.AlignBottom) 85 | return lbl 86 | 87 | 88 | class TetrixIndictor(QWidget): 89 | def __init__(self, parent = None): 90 | QWidget.__init__(self, parent) 91 | self.begin = self.end = None 92 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 93 | 94 | def showIndictor(self, curX, piece): 95 | self.begin = curX + piece.minX() 96 | self.end = curX + piece.maxX() 97 | self.update() 98 | 99 | def paintEvent(self, event): 100 | QWidget.paintEvent(self, event) 101 | if self.begin is None: 102 | return 103 | board = self.parent().board 104 | pieceWidth = board.contentsRect().width() // TetrixBoard.BoardWidth 105 | brush = QBrush(Qt.yellow) 106 | painter = QPainter(self) 107 | painter.setBrush(brush) 108 | painter.drawRect(board.contentsRect().left() + self.begin * pieceWidth, 0, \ 109 | (self.end - self.begin + 1) * pieceWidth, self.height() - 1 ) 110 | 111 | def sizeHint(self): 112 | return QSize(self.parent().board.width(), 8) 113 | 114 | 115 | class TetrixBoard(QFrame): 116 | BoardWidth = 11 117 | BoardHeight = 22 118 | 119 | scoreChanged = pyqtSignal(int) 120 | levelChanged = pyqtSignal(int) 121 | linesRemovedChanged = pyqtSignal(int) 122 | act = pyqtSignal(int, "PyQt_PyObject") 123 | 124 | def __init__(self, parent = None): 125 | super(TetrixBoard, self).__init__(parent) 126 | self.setStyleSheet("background-color:black;border:2px solid darkGreen;") 127 | 128 | self.timer = QBasicTimer() 129 | self.nextPieceLabel = None 130 | self.isWaitingAfterLine = False 131 | self.curPiece = TetrixPiece() 132 | self.nextPiece = TetrixPiece() 133 | self.curX = 0 134 | self.curY = 0 135 | self.numLinesRemoved = 0 136 | self.numPiecesDropped = 0 137 | self.score = 0 138 | self.level = 0 139 | self.board = None 140 | 141 | #self.setFrameStyle(QFrame.Panel | QFrame.Sunken) 142 | self.setFrameStyle(QFrame.Box) 143 | self.setFocusPolicy(Qt.StrongFocus) 144 | self.isStarted = False 145 | self.isPaused = False 146 | self.clearBoard() 147 | 148 | self.nextPiece.setRandomShape() 149 | 150 | def focusOutEvent(self, event): 151 | if self.isStarted and not self.isPaused: 152 | self.pause() 153 | QFrame.focusOutEvent(self, event) 154 | 155 | def shapeAt(self, x, y): 156 | return self.board[(y * TetrixBoard.BoardWidth) + x] 157 | 158 | def setShapeAt(self, x, y, shape): 159 | self.board[(y * TetrixBoard.BoardWidth) + x] = shape 160 | 161 | def timeoutTime(self): 162 | return 1000 // (1 + self.level) 163 | 164 | def squareWidth(self): 165 | return self.contentsRect().width() // TetrixBoard.BoardWidth 166 | 167 | def squareHeight(self): 168 | return self.contentsRect().height() // TetrixBoard.BoardHeight 169 | 170 | def setNextPieceLabel(self, label): 171 | self.nextPieceLabel = label 172 | #label.setScaledContents(True) 173 | label.setMinimumSize(label.width(), label.width()) 174 | 175 | def sizeHint(self): 176 | return QSize(TetrixBoard.BoardWidth * 15 + self.frameWidth() * 2, 177 | TetrixBoard.BoardHeight * 15 + self.frameWidth() * 2) 178 | 179 | def minimumSizeHint(self): 180 | return QSize(TetrixBoard.BoardWidth * 15 + self.frameWidth() * 2, 181 | TetrixBoard.BoardHeight * 15 + self.frameWidth() * 2) 182 | 183 | def start(self): 184 | if self.isPaused: 185 | return 186 | 187 | self.isStarted = True 188 | self.isWaitingAfterLine = False 189 | self.numLinesRemoved = 0 190 | self.numPiecesDropped = 0 191 | self.score = 0 192 | self.level = 1 193 | self.clearBoard() 194 | 195 | self.linesRemovedChanged.emit(self.numLinesRemoved) 196 | self.scoreChanged.emit(self.score) 197 | self.levelChanged.emit(self.level) 198 | 199 | self.newPiece() 200 | self.timer.start(self.timeoutTime(), self) 201 | 202 | def pause(self): 203 | if not self.isStarted: 204 | return 205 | 206 | self.isPaused = not self.isPaused 207 | if self.isPaused: 208 | self.timer.stop() 209 | else: 210 | self.timer.start(self.timeoutTime(), self) 211 | 212 | self.update() 213 | 214 | def paintEvent(self, event): 215 | super(TetrixBoard, self).paintEvent(event) 216 | 217 | painter = QPainter(self) 218 | rect = self.contentsRect() 219 | 220 | if self.isPaused: 221 | painter.drawText(rect, Qt.AlignCenter, self.tr("Pause")) 222 | return 223 | 224 | boardTop = rect.bottom() - TetrixBoard.BoardHeight * self.squareHeight() 225 | 226 | for i in range(TetrixBoard.BoardHeight): 227 | for j in range(TetrixBoard.BoardWidth): 228 | shape = self.shapeAt(j, TetrixBoard.BoardHeight - i - 1) 229 | if shape != NoShape: 230 | self.drawSquare(painter, 231 | rect.left() + j * self.squareWidth(), 232 | boardTop + i * self.squareHeight(), shape) 233 | 234 | if self.curPiece.shape() != NoShape: 235 | for i in range(4): 236 | x = self.curX + self.curPiece.x(i) 237 | y = self.curY - self.curPiece.y(i) 238 | self.drawSquare(painter, rect.left() + x * self.squareWidth(), 239 | boardTop + (TetrixBoard.BoardHeight - y - 1) * self.squareHeight(), 240 | self.curPiece.shape()) 241 | 242 | def keyPressEvent(self, event): 243 | if not self.isStarted or self.isPaused or self.curPiece.shape() == NoShape: 244 | super(TetrixBoard, self).keyPressEvent(event) 245 | return 246 | 247 | key = event.key() 248 | if key == Qt.Key_Left: 249 | self.tryMove(self.curPiece, self.curX - 1, self.curY) 250 | elif key == Qt.Key_Right: 251 | self.tryMove(self.curPiece, self.curX + 1, self.curY) 252 | elif key == Qt.Key_Down: 253 | self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY) 254 | elif key == Qt.Key_Up: 255 | self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY) 256 | elif key == Qt.Key_Space: 257 | self.dropDown() 258 | elif key == Qt.Key_D: 259 | self.oneLineDown() 260 | else: 261 | super(TetrixBoard, self).keyPressEvent(event) 262 | 263 | def timerEvent(self, event): 264 | if event.timerId() == self.timer.timerId(): 265 | if self.isWaitingAfterLine: 266 | self.isWaitingAfterLine = False 267 | self.newPiece() 268 | self.timer.start(self.timeoutTime(), self) 269 | else: 270 | self.oneLineDown() 271 | else: 272 | super(TetrixBoard, self).timerEvent(event) 273 | 274 | def clearBoard(self): 275 | self.board = [NoShape for i in range(TetrixBoard.BoardHeight * TetrixBoard.BoardWidth)] 276 | 277 | def dropDown(self): 278 | dropHeight = 0 279 | newY = self.curY 280 | while newY > 0: 281 | if not self.tryMove(self.curPiece, self.curX, newY - 1): 282 | break 283 | newY -= 1 284 | dropHeight += 1 285 | 286 | self.pieceDropped(dropHeight) 287 | 288 | def oneLineDown(self): 289 | if not self.tryMove(self.curPiece, self.curX, self.curY - 1): 290 | self.pieceDropped(0) 291 | 292 | def pieceDropped(self, dropHeight): 293 | for i in range(4): 294 | x = self.curX + self.curPiece.x(i) 295 | y = self.curY - self.curPiece.y(i) 296 | self.setShapeAt(x, y, self.curPiece.shape()) 297 | 298 | self.numPiecesDropped += 1 299 | if self.numPiecesDropped % 25 == 0: 300 | self.level += 1 301 | self.timer.start(self.timeoutTime(), self) 302 | self.levelChanged.emit(self.level) 303 | 304 | self.score += dropHeight + 7 305 | self.scoreChanged.emit(self.score) 306 | self.removeFullLines() 307 | 308 | if not self.isWaitingAfterLine: 309 | self.newPiece() 310 | 311 | def removeFullLines(self): 312 | numFullLines = 0 313 | 314 | for i in range(TetrixBoard.BoardHeight - 1, -1, -1): 315 | lineIsFull = True 316 | 317 | for j in range(TetrixBoard.BoardWidth): 318 | if self.shapeAt(j, i) == NoShape: 319 | lineIsFull = False 320 | break 321 | 322 | if lineIsFull: 323 | numFullLines += 1 324 | for k in range(i, TetrixBoard.BoardHeight - 1): 325 | for j in range(TetrixBoard.BoardWidth): 326 | self.setShapeAt(j, k, self.shapeAt(j, k + 1)) 327 | 328 | for j in range(TetrixBoard.BoardWidth): 329 | self.setShapeAt(j, TetrixBoard.BoardHeight - 1, NoShape) 330 | 331 | if numFullLines > 0: 332 | self.numLinesRemoved += numFullLines 333 | self.score += 10 * numFullLines 334 | self.linesRemovedChanged.emit(self.numLinesRemoved) 335 | self.scoreChanged.emit(self.score) 336 | 337 | self.timer.start(200, self) 338 | self.isWaitingAfterLine = True 339 | self.curPiece.setShape(NoShape) 340 | self.update() 341 | 342 | def newPiece(self): 343 | self.curPiece = self.nextPiece 344 | self.nextPiece = TetrixPiece() 345 | self.nextPiece.setRandomShape() 346 | self.showNextPiece() 347 | self.curX = TetrixBoard.BoardWidth // 2 348 | self.curY = TetrixBoard.BoardHeight - 1 + self.curPiece.minY() 349 | self.act.emit(self.curX, self.curPiece) 350 | 351 | if not self.tryMove(self.curPiece, self.curX, self.curY): 352 | self.curPiece.setShape(NoShape) 353 | self.timer.stop() 354 | self.isStarted = False 355 | 356 | def showNextPiece(self): 357 | if self.nextPieceLabel is None: 358 | return 359 | 360 | dx = self.nextPiece.maxX() - self.nextPiece.minX() + 1 361 | dy = self.nextPiece.maxY() - self.nextPiece.minY() + 1 362 | 363 | self.pixmapNextPiece = QPixmap(dx * self.squareWidth(), dy * self.squareHeight()) 364 | painter = QPainter(self.pixmapNextPiece) 365 | painter.fillRect(self.pixmapNextPiece.rect(), self.nextPieceLabel.palette().brush(QPalette.Background)) 366 | 367 | for i in range(4): 368 | x = self.nextPiece.x(i) - self.nextPiece.minX() 369 | y = self.nextPiece.y(i) - self.nextPiece.minY() 370 | self.drawSquare(painter, x * self.squareWidth(), 371 | y * self.squareHeight(), self.nextPiece.shape()) 372 | 373 | self.nextPieceLabel.setPixmap(self.pixmapNextPiece) 374 | 375 | def tryMove(self, newPiece, newX, newY): 376 | for i in range(4): 377 | x = newX + newPiece.x(i) 378 | y = newY - newPiece.y(i) 379 | if x < 0 or x >= TetrixBoard.BoardWidth or y < 0 or y >= TetrixBoard.BoardHeight: 380 | return False 381 | if self.shapeAt(x, y) != NoShape: 382 | return False 383 | 384 | self.curPiece = newPiece 385 | self.curX = newX 386 | self.curY = newY 387 | self.update() 388 | self.act.emit(self.curX, self.curPiece) 389 | return True 390 | 391 | def drawSquare(self, painter, x, y, shape): 392 | colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC, 393 | 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00] 394 | 395 | color = QColor(colorTable[shape]) 396 | painter.fillRect(x + 1, y + 1, self.squareWidth() - 2, 397 | self.squareHeight() - 2, color) 398 | 399 | painter.setPen(color.lighter()) 400 | painter.drawLine(x, y + self.squareHeight() - 1, x, y) 401 | painter.drawLine(x, y, x + self.squareWidth() - 1, y) 402 | 403 | painter.setPen(color.darker()) 404 | painter.drawLine(x + 1, y + self.squareHeight() - 1, 405 | x + self.squareWidth() - 1, y + self.squareHeight() - 1) 406 | painter.drawLine(x + self.squareWidth() - 1, 407 | y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1) 408 | 409 | 410 | class TetrixPiece(object): 411 | coordsTable = ( 412 | ((0, 0), (0, 0), (0, 0), (0, 0)), 413 | ((0, -1), (0, 0), ( - 1, 0), ( - 1, 1)), 414 | ((0, -1), (0, 0), (1, 0), (1, 1)), 415 | ((0, -1), (0, 0), (0, 1), (0, 2)), 416 | (( - 1, 0), (0, 0), (1, 0), (0, 1)), 417 | ((0, 0), (1, 0), (0, 1), (1, 1)), 418 | (( - 1, -1), (0, -1), (0, 0), (0, 1)), 419 | ((1, -1), (0, -1), (0, 0), (0, 1)) 420 | ) 421 | 422 | def __init__(self): 423 | self.coords = [[0,0] for _ in range(4)] 424 | self.pieceShape = NoShape 425 | 426 | self.setShape(NoShape) 427 | 428 | def shape(self): 429 | return self.pieceShape 430 | 431 | def setShape(self, shape): 432 | table = TetrixPiece.coordsTable[shape] 433 | for i in range(4): 434 | for j in range(2): 435 | self.coords[i][j] = table[i][j] 436 | 437 | self.pieceShape = shape 438 | 439 | def setRandomShape(self): 440 | self.setShape(random.randint(1, 7)) 441 | 442 | def x(self, index): 443 | return self.coords[index][0] 444 | 445 | def y(self, index): 446 | return self.coords[index][1] 447 | 448 | def setX(self, index, x): 449 | self.coords[index][0] = x 450 | 451 | def setY(self, index, y): 452 | self.coords[index][1] = y 453 | 454 | def minX(self): 455 | m = self.coords[0][0] 456 | for i in range(4): 457 | m = min(m, self.coords[i][0]) 458 | 459 | return m 460 | 461 | def maxX(self): 462 | m = self.coords[0][0] 463 | for i in range(4): 464 | m = max(m, self.coords[i][0]) 465 | 466 | return m 467 | 468 | def minY(self): 469 | m = self.coords[0][1] 470 | for i in range(4): 471 | m = min(m, self.coords[i][1]) 472 | 473 | return m 474 | 475 | def maxY(self): 476 | m = self.coords[0][1] 477 | for i in range(4): 478 | m = max(m, self.coords[i][1]) 479 | 480 | return m 481 | 482 | def rotatedLeft(self): 483 | if self.pieceShape == SquareShape: 484 | return self 485 | 486 | result = TetrixPiece() 487 | result.pieceShape = self.pieceShape 488 | for i in range(4): 489 | result.setX(i, self.y(i)) 490 | result.setY(i, -self.x(i)) 491 | 492 | return result 493 | 494 | def rotatedRight(self): 495 | if self.pieceShape == SquareShape: 496 | return self 497 | 498 | result = TetrixPiece() 499 | result.pieceShape = self.pieceShape 500 | for i in range(4): 501 | result.setX(i, -self.y(i)) 502 | result.setY(i, self.x(i)) 503 | 504 | return result 505 | 506 | if __name__ == '__main__': 507 | app = QApplication(sys.argv) 508 | window = TetrixWindow() 509 | window.show() 510 | if hasattr(app, "exec"): 511 | result = getattr(app, "exec")() 512 | else: 513 | result = getattr(app, "exec_")() 514 | sys.exit(result) 515 | 516 | -------------------------------------------------------------------------------- /wc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import io 5 | 6 | join = os.path.join 7 | basename = os.path.basename 8 | 9 | def countLine(filename): 10 | lines = 0 11 | blankLines = 0 12 | commentedLines = 0 13 | with io.open(filename, "r", encoding = "utf-8") as f: 14 | while True: 15 | line = f.readline() 16 | if line is None or line == "": 17 | break 18 | lines += 1 19 | if line == "\n": 20 | blankLines += 1 21 | elif line.lstrip().startswith(("//", "#")): 22 | commentedLines += 1 23 | return lines, blankLines, commentedLines 24 | 25 | def findCppFiles(dirname): 26 | cppFiles = [] 27 | for filename in os.listdir(dirname): 28 | if os.path.isdir(filename): 29 | d = join(dirname, filename) 30 | cppFiles.extend(findCppFiles(d)) 31 | if filename.lower().startswith("ui_"): 32 | continue 33 | if filename.lower().endswith((".hpp", ".cpp", ".c", ".h")): 34 | cppFiles.append(join(dirname, filename)) 35 | return cppFiles 36 | 37 | def findPyFiles(dirname): 38 | pyFiles = [] 39 | for filename in os.listdir(dirname): 40 | if os.path.isdir(join(dirname, filename)): 41 | d = join(dirname, filename) 42 | pyFiles.extend(findPyFiles(d)) 43 | if filename.startswith("Ui_"): 44 | continue 45 | if filename.endswith("_rc.py"): 46 | continue 47 | if filename.lower().endswith((".py", ".pyw")): 48 | pyFiles.append(join(dirname, filename)) 49 | return pyFiles 50 | 51 | if __name__ == "__main__": 52 | print("filename".ljust(25) + "bytes".ljust(12) + "total lines".ljust(12) + \ 53 | "blank lines".ljust(12) + "commented lines".ljust(12)) 54 | print("=" * 79) 55 | totalBytes = 0 56 | totalLines = 0 57 | totalBlankLines = 0 58 | totalCommentedLines = 0 59 | for filename in findPyFiles("."): 60 | bytes = os.path.getsize(filename) 61 | lines, blankLines, commentedLines = countLine(filename) 62 | totalBytes += bytes 63 | totalLines += lines 64 | totalBlankLines += blankLines 65 | totalCommentedLines += commentedLines 66 | print(basename(filename).ljust(25) + str(bytes).ljust(12) + str(lines).ljust(12) + \ 67 | str(blankLines).ljust(12) + str(commentedLines).ljust(12)) 68 | print("=" * 79) 69 | print(" " * 25 + str(totalBytes).ljust(12) + str(totalLines).ljust(12) + \ 70 | str(totalBlankLines).ljust(12) + str(totalCommentedLines).ljust(12)) 71 | --------------------------------------------------------------------------------