a better main window

This commit is contained in:
dries.k
2022-02-08 22:12:52 +01:00
parent 6c280606b2
commit 0bea6ea14f
27 changed files with 1294 additions and 37 deletions

View File

@@ -3,23 +3,35 @@ import logging
import os
import sys
import tempfile
from typing import List
from typing import List, Union
from gotify_tray import gotify
from gotify_tray.__version__ import __title__
from gotify_tray.database import Downloader, Settings
from gotify_tray.tasks import GetApplicationsTask, ServerConnectionWatchdogTask
from gotify_tray.tasks import (
DeleteApplicationMessagesTask,
DeleteAllMessagesTask,
DeleteMessageTask,
GetApplicationsTask,
GetApplicationMessagesTask,
GetMessagesTask,
ServerConnectionWatchdogTask,
)
from gotify_tray.utils import verify_server
from PyQt6 import QtCore, QtGui, QtWidgets
from ..__version__ import __title__
from .ApplicationModel import (
from .models import (
ApplicationAllMessagesItem,
ApplicationItemDataRole,
ApplicationModel,
ApplicationModelItem,
MessagesModel,
MessagesModelItem,
MessageItemDataRole,
)
from .SettingsDialog import SettingsDialog
from .Tray import Tray
from .widgets import MainWindow, SettingsDialog, Tray
settings = Settings("gotify-tray")
logger = logging.getLogger("gotify-tray")
@@ -58,7 +70,11 @@ class MainApplication(QtWidgets.QApplication):
self.downloader = Downloader()
self.messages_model = MessagesModel()
self.application_model = ApplicationModel()
self.main_window = MainWindow(self.application_model, self.messages_model)
self.refresh_applications()
self.tray = Tray()
@@ -77,44 +93,144 @@ class MainApplication(QtWidgets.QApplication):
self.watchdog.start()
def refresh_applications(self):
self.messages_model.clear()
self.application_model.clear()
def get_applications_callback(
applications: List[gotify.GotifyApplicationModel],
):
for i, application in enumerate(applications):
icon = QtGui.QIcon(
self.downloader.get_filename(
f"{self.gotify_client.url}/{application.image}"
)
)
self.application_model.setItem(
i, 0, ApplicationModelItem(application, icon),
)
self.application_model.setItem(0, 0, ApplicationAllMessagesItem())
self.get_applications_task = GetApplicationsTask(self.gotify_client)
self.get_applications_task.success.connect(get_applications_callback)
self.get_applications_task.success.connect(
self.get_applications_success_callback
)
self.get_applications_task.started.connect(
self.main_window.disable_applications
)
self.get_applications_task.finished.connect(
self.main_window.enable_applications
)
self.get_applications_task.start()
def get_applications_success_callback(
self, applications: List[gotify.GotifyApplicationModel],
):
for i, application in enumerate(applications):
icon = QtGui.QIcon(
self.downloader.get_filename(
f"{self.gotify_client.url}/{application.image}"
)
)
self.application_model.setItem(
i + 1, 0, ApplicationModelItem(application, icon),
)
def listener_opened_callback(self):
self.main_window.set_active()
self.tray.set_icon_ok()
# self.tray.actionReconnect.setEnabled(True)
def listener_closed_callback(self, close_status_code: int, close_msg: str):
self.main_window.set_connecting()
self.tray.set_icon_error()
if not self.shutting_down:
self.gotify_client.reconnect()
def refresh_callback(self):
# self.tray.actionReconnect.setDisabled(True)
def reconnect_callback(self):
if not self.gotify_client.is_listening():
self.gotify_client.listener.reset_wait_time()
else:
self.gotify_client.stop(reset_wait=True)
self.gotify_client.reconnect(increase_wait_time=False)
def insert_message(
self,
row: int,
message: gotify.GotifyMessageModel,
application: gotify.GotifyApplicationModel,
):
message_item = MessagesModelItem(message)
self.messages_model.insertRow(row, message_item)
self.main_window.insert_message_widget(
message_item,
self.downloader.get_filename(
f"{self.gotify_client.url}/{application.image}"
),
)
def application_selection_changed_callback(
self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem]
):
self.messages_model.clear()
if isinstance(item, ApplicationModelItem):
def get_application_messages_callback(
page: gotify.GotifyPagedMessagesModel,
):
for i, message in enumerate(page.messages):
self.insert_message(
i, message, item.data(ApplicationItemDataRole.ApplicationRole),
)
self.get_application_messages_task = GetApplicationMessagesTask(
item.data(ApplicationItemDataRole.ApplicationRole).id,
self.gotify_client,
)
self.get_application_messages_task.success.connect(
get_application_messages_callback
)
self.get_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem):
def get_messages_callback(page: gotify.GotifyPagedMessagesModel):
for i, message in enumerate(page.messages):
if item := self.application_model.itemFromId(message.appid):
self.insert_message(
i,
message,
item.data(ApplicationItemDataRole.ApplicationRole),
)
self.get_messages_task = GetMessagesTask(self.gotify_client)
self.get_messages_task.success.connect(get_messages_callback)
self.get_messages_task.start()
def add_message_to_model(self, message: gotify.GotifyMessageModel):
if application_item := self.application_model.itemFromId(message.appid):
application_index = self.main_window.currentApplicationIndex()
if selected_application_item := self.application_model.itemFromIndex(
application_index
):
if isinstance(selected_application_item, ApplicationModelItem):
# A single application is selected
if (
message.appid
== selected_application_item.data(
ApplicationItemDataRole.ApplicationRole
).id
):
self.insert_message(
0,
message,
application_item.data(
ApplicationItemDataRole.ApplicationRole
),
)
elif isinstance(selected_application_item, ApplicationAllMessagesItem):
# "All messages' is selected
self.insert_message(
0,
message,
application_item.data(ApplicationItemDataRole.ApplicationRole),
)
def new_message_callback(self, message: gotify.GotifyMessageModel):
if message.priority < settings.value("tray/notifications/priority", type=int):
self.add_message_to_model(message)
# Show a notification
if (
message.priority < settings.value("tray/notifications/priority", type=int)
or self.main_window.isActiveWindow()
):
return
if settings.value("tray/notifications/icon/show", type=bool):
@@ -130,7 +246,6 @@ class MainApplication(QtWidgets.QApplication):
else:
icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information
# Show a notification
self.tray.showMessage(
message.title,
message.message,
@@ -138,6 +253,30 @@ class MainApplication(QtWidgets.QApplication):
msecs=settings.value("tray/notifications/duration_ms", type=int),
)
def delete_message_callback(self, message_item: MessagesModelItem):
self.delete_message_task = DeleteMessageTask(
message_item.data(MessageItemDataRole.MessageRole).id, self.gotify_client
)
self.messages_model.removeRow(message_item.row())
self.delete_message_task.start()
def delete_all_messages_callback(
self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem]
):
if isinstance(item, ApplicationModelItem):
self.delete_application_messages_task = DeleteApplicationMessagesTask(
item.data(ApplicationItemDataRole.ApplicationRole).id,
self.gotify_client,
)
self.delete_application_messages_task.start()
elif isinstance(item, ApplicationAllMessagesItem):
self.delete_all_messages_task = DeleteAllMessagesTask(self.gotify_client)
self.delete_all_messages_task.start()
else:
return
self.messages_model.clear()
def settings_callback(self):
settings_dialog = SettingsDialog()
accepted = settings_dialog.exec()
@@ -158,10 +297,26 @@ class MainApplication(QtWidgets.QApplication):
if r == QtWidgets.QMessageBox.StandardButton.Yes:
self.quit()
def tray_activated_callback(
self, reason: QtWidgets.QSystemTrayIcon.ActivationReason
):
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
self.main_window.bring_to_front()
def link_callbacks(self):
self.tray.actionQuit.triggered.connect(self.quit)
self.tray.actionSettings.triggered.connect(self.settings_callback)
self.tray.actionReconnect.triggered.connect(self.refresh_callback)
self.tray.actionShowWindow.triggered.connect(self.main_window.bring_to_front)
self.tray.actionReconnect.triggered.connect(self.reconnect_callback)
self.tray.messageClicked.connect(self.main_window.bring_to_front)
self.tray.activated.connect(self.tray_activated_callback)
self.main_window.refresh.connect(self.refresh_applications)
self.main_window.delete_all.connect(self.delete_all_messages_callback)
self.main_window.application_selection_changed.connect(
self.application_selection_changed_callback
)
self.main_window.delete_message.connect(self.delete_message_callback)
self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None))
@@ -175,6 +330,8 @@ class MainApplication(QtWidgets.QApplication):
return self.lock_file.tryLock()
def quit(self) -> None:
self.main_window.store_state()
self.tray.hide()
self.lock_file.unlock()

View File

@@ -1,2 +1,2 @@
from .MainApplication import start_gui
from .ServerInfoDialog import ServerInfoDialog
from .widgets import ServerInfoDialog

View File

@@ -0,0 +1,77 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_main.ui'
#
# Created by: PyQt6 UI code generator 6.1.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(809, 647)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setContentsMargins(4, 4, 4, 4)
self.gridLayout.setObjectName("gridLayout")
self.splitter = QtWidgets.QSplitter(self.centralwidget)
self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.splitter.setObjectName("splitter")
self.listView_applications = QtWidgets.QListView(self.splitter)
self.listView_applications.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.listView_applications.setObjectName("listView_applications")
self.verticalLayoutWidget = QtWidgets.QWidget(self.splitter)
self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.label_application = QtWidgets.QLabel(self.verticalLayoutWidget)
self.label_application.setObjectName("label_application")
self.horizontalLayout.addWidget(self.label_application)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.pb_refresh = QtWidgets.QPushButton(self.verticalLayoutWidget)
self.pb_refresh.setText("")
self.pb_refresh.setObjectName("pb_refresh")
self.horizontalLayout.addWidget(self.pb_refresh)
self.pb_delete_all = QtWidgets.QPushButton(self.verticalLayoutWidget)
self.pb_delete_all.setText("")
self.pb_delete_all.setObjectName("pb_delete_all")
self.horizontalLayout.addWidget(self.pb_delete_all)
self.verticalLayout_2.addLayout(self.horizontalLayout)
self.listView_messages = QtWidgets.QListView(self.verticalLayoutWidget)
self.listView_messages.setAutoScroll(True)
self.listView_messages.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.listView_messages.setObjectName("listView_messages")
self.verticalLayout_2.addWidget(self.listView_messages)
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
MainWindow.setTabOrder(self.listView_applications, self.listView_messages)
MainWindow.setTabOrder(self.listView_messages, self.pb_refresh)
MainWindow.setTabOrder(self.pb_refresh, self.pb_delete_all)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Form"))
self.label_application.setText(_translate("MainWindow", "Application"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>809</width>
<height>647</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QListView" name="listView_applications">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_application">
<property name="text">
<string>Application</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pb_refresh">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_delete_all">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="listView_messages">
<property name="autoScroll">
<bool>true</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>listView_applications</tabstop>
<tabstop>listView_messages</tabstop>
<tabstop>pb_refresh</tabstop>
<tabstop>pb_delete_all</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,94 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_message.ui'
#
# Created by: PyQt6 UI code generator 6.1.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(454, 122)
self.gridLayout = QtWidgets.QGridLayout(Form)
self.gridLayout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.frame = QtWidgets.QFrame(Form)
self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame.setObjectName("frame")
self.gridLayout_frame = QtWidgets.QGridLayout(self.frame)
self.gridLayout_frame.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize)
self.gridLayout_frame.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_frame.setObjectName("gridLayout_frame")
self.label_title = QtWidgets.QLabel(self.frame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label_title.sizePolicy().hasHeightForWidth())
self.label_title.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(17)
font.setBold(False)
font.setWeight(50)
self.label_title.setFont(font)
self.label_title.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.label_title.setObjectName("label_title")
self.gridLayout_frame.addWidget(self.label_title, 0, 1, 1, 1)
self.text_message = QtWidgets.QLabel(self.frame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.text_message.sizePolicy().hasHeightForWidth())
self.text_message.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(11)
self.text_message.setFont(font)
self.text_message.setWordWrap(True)
self.text_message.setOpenExternalLinks(True)
self.text_message.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.text_message.setObjectName("text_message")
self.gridLayout_frame.addWidget(self.text_message, 3, 1, 1, 3)
self.label_date = QtWidgets.QLabel(self.frame)
font = QtGui.QFont()
font.setPointSize(11)
self.label_date.setFont(font)
self.label_date.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse|QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.label_date.setObjectName("label_date")
self.gridLayout_frame.addWidget(self.label_date, 2, 1, 1, 1)
self.pb_delete = QtWidgets.QPushButton(self.frame)
self.pb_delete.setText("")
self.pb_delete.setFlat(True)
self.pb_delete.setObjectName("pb_delete")
self.gridLayout_frame.addWidget(self.pb_delete, 0, 3, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_frame.addItem(spacerItem, 0, 2, 1, 1)
self.label_image = QtWidgets.QLabel(self.frame)
self.label_image.setText("")
self.label_image.setObjectName("label_image")
self.gridLayout_frame.addWidget(self.label_image, 0, 0, 1, 1)
self.gridLayout.addWidget(self.frame, 0, 0, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.label_title.setText(_translate("Form", "Title"))
self.text_message.setText(_translate("Form", "TextLabel"))
self.label_date.setText(_translate("Form", "Date"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>454</width>
<height>122</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_frame">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="label_title">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>17</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Title</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="1" colspan="3">
<widget class="QLabel" name="text_message">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_date">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Date</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="pb_delete">
<property name="text">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_image">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_server.ui'
#
# Created by: PyQt6 UI code generator 6.2.1
# Created by: PyQt6 UI code generator 6.1.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
@@ -46,8 +46,8 @@ class Ui_Dialog(object):
self.gridLayout.addWidget(self.label_server_info, 1, 1, 1, 2)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'gotify_tray/gui/designs\widget_settings.ui'
#
# Created by: PyQt6 UI code generator 6.2.1
# Created by: PyQt6 UI code generator 6.1.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
@@ -80,8 +80,8 @@ class Ui_Dialog(object):
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QtCore.QMetaObject.connectSlotsByName(Dialog)
Dialog.setTabOrder(self.cb_icons_notification, self.spin_priority)
Dialog.setTabOrder(self.spin_priority, self.spin_duration)

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 492.883 492.883" style="enable-background:new 0 0 492.883 492.883;" xml:space="preserve">
<g>
<g>
<path d="M122.941,374.241c-20.1-18.1-34.6-39.8-44.1-63.1c-25.2-61.8-13.4-135.3,35.8-186l45.4,45.4c2.5,2.5,7,0.7,7.6-3
l24.8-162.3c0.4-2.7-1.9-5-4.6-4.6l-162.4,24.8c-3.7,0.6-5.5,5.1-3,7.6l45.5,45.5c-75.1,76.8-87.9,192-38.6,282
c14.8,27.1,35.3,51.9,61.4,72.7c44.4,35.3,99,52.2,153.2,51.1l10.2-66.7C207.441,421.641,159.441,407.241,122.941,374.241z"/>
<path d="M424.941,414.341c75.1-76.8,87.9-192,38.6-282c-14.8-27.1-35.3-51.9-61.4-72.7c-44.4-35.3-99-52.2-153.2-51.1l-10.2,66.7
c46.6-4,94.7,10.4,131.2,43.4c20.1,18.1,34.6,39.8,44.1,63.1c25.2,61.8,13.4,135.3-35.8,186l-45.4-45.4c-2.5-2.5-7-0.7-7.6,3
l-24.8,162.3c-0.4,2.7,1.9,5,4.6,4.6l162.4-24.8c3.7-0.6,5.4-5.1,3-7.6L424.941,414.341z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.348576mm"
height="29.348576mm"
viewBox="0 0 29.348576 29.348576"
version="1.1"
id="svg5"
sodipodi:docname="status_ok.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.77771465"
inkscape:cx="188.37243"
inkscape:cy="505.96964"
inkscape:window-width="1284"
inkscape:window-height="1082"
inkscape:window-x="1622"
inkscape:window-y="248"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-24.619528,-14.753546)">
<circle
style="fill:#00db00;fill-opacity:1;stroke-width:0.264583"
id="path846"
cx="39.293816"
cy="29.427834"
r="14.674288" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.348576mm"
height="29.348576mm"
viewBox="0 0 29.348576 29.348576"
version="1.1"
id="svg5"
sodipodi:docname="status_connecting.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.77771465"
inkscape:cx="188.37243"
inkscape:cy="505.96964"
inkscape:window-width="1284"
inkscape:window-height="1082"
inkscape:window-x="1852"
inkscape:window-y="271"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-24.619528,-14.753546)">
<circle
style="fill:#ffb200;fill-opacity:1;stroke-width:0.264583"
id="path846"
cx="39.293816"
cy="29.427834"
r="14.674288" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.348576mm"
height="29.348576mm"
viewBox="0 0 29.348576 29.348576"
version="1.1"
id="svg5"
sodipodi:docname="status_error.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.77771465"
inkscape:cx="188.37243"
inkscape:cy="505.96964"
inkscape:window-width="1284"
inkscape:window-height="1082"
inkscape:window-x="2013"
inkscape:window-y="263"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-24.619528,-14.753546)">
<circle
style="fill:#ff3232;fill-opacity:1;stroke-width:0.264583"
id="path846"
cx="39.293816"
cy="29.427834"
r="14.674288" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.348576mm"
height="29.348576mm"
viewBox="0 0 29.348576 29.348576"
version="1.1"
id="svg5"
sodipodi:docname="status_inactive.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.77771465"
inkscape:cx="188.37243"
inkscape:cy="505.96964"
inkscape:window-width="1284"
inkscape:window-height="1082"
inkscape:window-x="1852"
inkscape:window-y="271"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-24.619528,-14.753546)">
<circle
style="fill:#b4b4b4;fill-opacity:1;stroke-width:0.264583"
id="path846"
cx="39.293816"
cy="29.427834"
r="14.674288" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -1,6 +1,6 @@
import enum
from typing import Optional
from typing import Optional, Union
from PyQt6 import QtCore, QtGui
from gotify_tray import gotify
from gotify_tray.database import Settings
@@ -20,12 +20,15 @@ class ApplicationModelItem(QtGui.QStandardItem):
application: gotify.GotifyApplicationModel,
icon: Optional[QtGui.QIcon] = None,
*args,
**kwargs
**kwargs,
):
super(ApplicationModelItem, self).__init__(application.name)
self.setDropEnabled(False)
self.setData(application, ApplicationItemDataRole.ApplicationRole)
self.setData(icon, ApplicationItemDataRole.IconRole)
font = QtGui.QFont()
font.fromString(settings.value("ApplicationItem/font", type=str))
self.setFont(font)
if icon:
self.setIcon(icon)
@@ -36,6 +39,16 @@ class ApplicationModelItem(QtGui.QStandardItem):
)
class ApplicationAllMessagesItem(QtGui.QStandardItem):
def __init__(self, *args, **kwargs):
super(ApplicationAllMessagesItem, self).__init__("ALL MESSAGES")
self.setDropEnabled(False)
self.setDragEnabled(False)
font = QtGui.QFont()
font.fromString(settings.value("ApplicationItem/font", type=str))
self.setFont(font)
class ApplicationModel(QtGui.QStandardItemModel):
def __init__(self):
super(ApplicationModel, self).__init__()
@@ -43,10 +56,17 @@ class ApplicationModel(QtGui.QStandardItemModel):
ApplicationModelItem(gotify.GotifyApplicationModel({"name": ""}), None)
)
def setItem(self, row: int, column: int, item: ApplicationModelItem,) -> None:
def setItem(
self,
row: int,
column: int,
item: Union[ApplicationModelItem, ApplicationAllMessagesItem],
) -> None:
super(ApplicationModel, self).setItem(row, column, item)
def itemFromIndex(self, index: QtCore.QModelIndex) -> ApplicationModelItem:
def itemFromIndex(
self, index: QtCore.QModelIndex
) -> Union[ApplicationModelItem, ApplicationAllMessagesItem]:
return super(ApplicationModel, self).itemFromIndex(index)
def itemFromId(self, appid: int) -> Optional[ApplicationModelItem]:

View File

@@ -0,0 +1,23 @@
import enum
from typing import cast
from PyQt6 import QtCore, QtGui
from gotify_tray import gotify
class MessageItemDataRole(enum.IntEnum):
MessageRole = QtCore.Qt.ItemDataRole.UserRole + 1
class MessagesModelItem(QtGui.QStandardItem):
def __init__(self, message: gotify.GotifyMessageModel, *args, **kwargs):
super(MessagesModelItem, self).__init__()
self.setData(message, MessageItemDataRole.MessageRole)
class MessagesModel(QtGui.QStandardItemModel):
def setItem(self, row: int, column: int, item: MessagesModelItem) -> None:
super(MessagesModel, self).setItem(row, column, item)
def itemFromIndex(self, index: QtCore.QModelIndex) -> MessagesModelItem:
return cast(MessagesModelItem, super(MessagesModel, self).itemFromIndex(index))

View File

@@ -0,0 +1,7 @@
from .ApplicationModel import (
ApplicationAllMessagesItem,
ApplicationModelItem,
ApplicationModel,
ApplicationItemDataRole,
)
from .MessagesModel import MessagesModelItem, MessagesModel, MessageItemDataRole

View File

@@ -0,0 +1,154 @@
from PyQt6 import QtCore, QtGui, QtWidgets
from ..designs.widget_main import Ui_MainWindow
from .StatusWidget import StatusWidget
from ..models import (
ApplicationModel,
MessagesModel,
MessagesModelItem,
)
from . import MessageWidget
from gotify_tray.__version__ import __title__
from gotify_tray.database import Settings
from gotify_tray.utils import get_abs_path
settings = Settings("gotify-tray")
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
refresh = QtCore.pyqtSignal()
delete_all = QtCore.pyqtSignal(QtGui.QStandardItem)
delete_message = QtCore.pyqtSignal(MessagesModelItem)
application_selection_changed = QtCore.pyqtSignal(QtGui.QStandardItem)
def __init__(
self, application_model: ApplicationModel, messages_model: MessagesModel
):
super(MainWindow, self).__init__()
self.setupUi(self)
self.setWindowTitle(__title__)
self.application_model = application_model
self.messages_model = messages_model
self.listView_applications.setModel(application_model)
self.listView_messages.setModel(messages_model)
# Do not expand the applications listview when resizing
self.splitter.setStretchFactor(0, 0)
self.splitter.setStretchFactor(1, 1)
self.status_widget = StatusWidget()
self.horizontalLayout.insertWidget(0, self.status_widget)
# Set button icons
self.pb_refresh.setIcon(
QtGui.QIcon(get_abs_path(f"gotify_tray/gui/images/refresh.svg"))
)
self.pb_delete_all.setIcon(
QtGui.QIcon(get_abs_path(f"gotify_tray/gui/images/trashcan.svg"))
)
# Resize the labels and icons
size = settings.value("MainWindow/label/size", type=int)
self.status_widget.setFixedSize(QtCore.QSize(size, size))
size = settings.value("MainWindow/button/size", type=int)
self.pb_refresh.setFixedSize(QtCore.QSize(size, size))
self.pb_delete_all.setFixedSize(QtCore.QSize(size, size))
self.pb_refresh.setIconSize(QtCore.QSize(0.7 * size, 0.7 * size))
self.pb_delete_all.setIconSize(QtCore.QSize(0.9 * size, 0.9 * size))
size = settings.value("MainWindow/application/icon/size", type=int)
self.listView_applications.setIconSize(QtCore.QSize(size, size))
font_title = QtGui.QFont()
font_title.fromString(settings.value("MainWindow/font/application", type=str))
self.label_application.setFont(font_title)
# Set tooltips
self.pb_refresh.setToolTip("Refresh")
self.pb_delete_all.setToolTip("Delete all messages")
self.restore_state()
self.link_callbacks()
def set_active(self):
self.status_widget.set_active()
def set_connecting(self):
self.status_widget.set_connecting()
def set_inactive(self):
self.status_widget.set_inactive()
def set_error(self):
self.status_widget.set_error()
def insert_message_widget(
self, message_item: MessagesModelItem, image_path: str = ""
):
message_widget = MessageWidget(message_item, image_path=image_path)
self.listView_messages.setIndexWidget(
self.messages_model.indexFromItem(message_item), message_widget
)
message_widget.deletion_requested.connect(self.delete_message.emit)
def currentApplicationIndex(self) -> QtCore.QModelIndex:
return self.listView_applications.selectionModel().currentIndex()
def application_selection_changed_callback(
self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex
):
if item := self.application_model.itemFromIndex(current):
self.label_application.setText(item.text())
self.application_selection_changed.emit(item)
def delete_all_callback(self):
index = self.currentApplicationIndex()
if item := self.application_model.itemFromIndex(index):
self.delete_all.emit(item)
def disable_applications(self):
self.listView_applications.clearSelection()
self.listView_applications.setDisabled(True)
def enable_applications(self):
self.listView_applications.setEnabled(True)
self.listView_applications.setCurrentIndex(self.application_model.index(0, 0))
def bring_to_front(self):
self.ensurePolished()
self.setWindowState(
self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized
| QtCore.Qt.WindowState.WindowActive
)
self.show()
self.activateWindow()
def link_callbacks(self):
self.pb_refresh.clicked.connect(self.refresh.emit)
self.pb_delete_all.clicked.connect(self.delete_all_callback)
self.listView_applications.selectionModel().currentChanged.connect(
self.application_selection_changed_callback
)
def store_state(self):
settings.setValue("MainWindow/geometry", self.saveGeometry())
settings.setValue("MainWindow/state", self.saveState())
settings.setValue("MainWindow/splitter", self.splitter.saveState())
def restore_state(self):
if geometry := settings.value("MainWindow/geometry", type=QtCore.QByteArray):
self.restoreGeometry(geometry)
if state := settings.value("MainWindow/state", type=QtCore.QByteArray):
self.restoreState(state)
if splitter := settings.value("MainWindow/splitter", type=QtCore.QByteArray):
self.splitter.restoreState(splitter)
def closeEvent(self, e: QtGui.QCloseEvent) -> None:
self.hide()

View File

@@ -0,0 +1,71 @@
from PyQt6 import QtCore, QtGui, QtWidgets
from ..models.MessagesModel import MessageItemDataRole, MessagesModelItem
from ..designs.widget_message import Ui_Form
from gotify_tray.database import Settings
from gotify_tray.utils import convert_links
settings = Settings("gotify-tray")
class MessageWidget(QtWidgets.QWidget, Ui_Form):
deletion_requested = QtCore.pyqtSignal(MessagesModelItem)
def __init__(self, message_item: MessagesModelItem, image_path: str = ""):
super(MessageWidget, self).__init__()
self.setupUi(self)
self.setAutoFillBackground(True)
self.message_item = message_item
message = message_item.data(MessageItemDataRole.MessageRole)
# Fonts
font_title = QtGui.QFont()
font_date = QtGui.QFont()
font_content = QtGui.QFont()
font_title.fromString(settings.value("MessageWidget/font/title", type=str))
font_date.fromString(settings.value("MessageWidget/font/date", type=str))
font_content.fromString(settings.value("MessageWidget/font/content", type=str))
self.label_title.setFont(font_title)
self.label_date.setFont(font_date)
self.text_message.setFont(font_content)
# Display message contents
self.label_title.setText(message.title)
self.label_date.setText(message.date.strftime("%Y-%m-%d, %H:%M"))
if markdown := message.get("extras", {}).get("client::display", {}).get("contentType") == "text/markdown":
self.text_message.setTextFormat(QtCore.Qt.TextFormat.MarkdownText)
self.text_message.setText(convert_links(message.message))
# Show the application icon
if image_path:
image_size = settings.value("MessageWidget/image/size", type=int)
self.label_image.setFixedSize(QtCore.QSize(image_size, image_size))
pixmap = QtGui.QPixmap(image_path).scaled(image_size, image_size, aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatioByExpanding)
self.label_image.setPixmap(pixmap)
else:
self.label_image.hide()
# Set MessagesModelItem's size hint based on the size of this widget
self.gridLayout_frame.setContentsMargins(10, 5, 10, 5)
self.gridLayout.setContentsMargins(5, 15, 5, 15)
self.adjustSize()
size_hint = self.message_item.sizeHint()
self.message_item.setSizeHint(
QtCore.QSize(
size_hint.width(),
self.height()
)
)
self.pb_delete.setIcon(QtGui.QIcon("gotify_tray/gui/images/trashcan.svg"))
self.pb_delete.setIconSize(QtCore.QSize(24, 24))
self.link_callbacks()
def link_callbacks(self):
self.pb_delete.clicked.connect(lambda: self.deletion_requested.emit(self.message_item))

View File

@@ -2,7 +2,7 @@ from gotify_tray.gotify.models import GotifyVersionModel
from gotify_tray.tasks import VerifyServerInfoTask
from PyQt6 import QtWidgets
from .designs.widget_server import Ui_Dialog
from ..designs.widget_server import Ui_Dialog
class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog):

View File

@@ -5,7 +5,7 @@ from gotify_tray.database import Settings
from gotify_tray.utils import verify_server
from PyQt6 import QtCore, QtGui, QtWidgets
from .designs.widget_settings import Ui_Dialog
from ..designs.widget_settings import Ui_Dialog
logger = logging.getLogger("gotify-tray")

View File

@@ -0,0 +1,34 @@
from PyQt6 import QtCore, QtGui, QtWidgets
from gotify_tray.database import Settings
from gotify_tray.utils import get_abs_path
settings = Settings("gotify-tray")
class StatusWidget(QtWidgets.QLabel):
def __init__(self):
super(StatusWidget, self).__init__()
self.setFixedSize(QtCore.QSize(20, 20))
self.setScaledContents(True)
self.set_connecting()
def set_status(self, image: str):
self.setPixmap(QtGui.QPixmap(get_abs_path(f"gotify_tray/gui/images/{image}")))
def set_active(self):
self.setToolTip("Listening for new messages")
self.set_status("status_active.svg")
def set_connecting(self):
self.setToolTip("Connecting...")
self.set_status("status_connecting.svg")
def set_inactive(self):
self.setToolTip("Listener inactive")
self.set_status("status_inactive.svg")
def set_error(self):
self.setToolTip("Listener error")
self.set_status("status_error.svg")

View File

@@ -27,6 +27,11 @@ class Tray(QtWidgets.QSystemTrayIcon):
menu.addSeparator()
self.actionShowWindow = QtGui.QAction("Show Window", self)
menu.addAction(self.actionShowWindow)
menu.addSeparator()
self.actionReconnect = QtGui.QAction("Reconnect", self)
menu.addAction(self.actionReconnect)

View File

@@ -0,0 +1,6 @@
from .MessageWidget import MessageWidget
from .MainWindow import MainWindow
from .ServerInfoDialog import ServerInfoDialog
from .SettingsDialog import SettingsDialog
from .StatusWidget import StatusWidget
from .Tray import Tray