├── 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 |
--------------------------------------------------------------------------------