diff --git a/buildPy2app.py b/buildPy2app.py
index 68e6466..659e575 100755
--- a/buildPy2app.py
+++ b/buildPy2app.py
@@ -30,7 +30,8 @@ OPTIONS = {
'CFBundleShortVersionString': syncplay.version,
'CFBundleIdentifier': 'pl.syncplay.Syncplay',
'LSMinimumSystemVersion': '10.12.0',
- 'NSHumanReadableCopyright': 'Copyright © 2019 Syncplay All Rights Reserved'
+ 'NSHumanReadableCopyright': 'Copyright © 2019 Syncplay All Rights Reserved',
+ 'NSRequiresAquaSystemAppearance': False,
}
}
diff --git a/syncplay/constants.py b/syncplay/constants.py
index 2ed523c..f200d9e 100755
--- a/syncplay/constants.py
+++ b/syncplay/constants.py
@@ -212,6 +212,14 @@ STYLE_NOFILEITEM_COLOR = 'blue'
STYLE_NOTCONTROLLER_COLOR = 'grey'
STYLE_UNTRUSTEDITEM_COLOR = 'purple'
+STYLE_DARK_LINKS_COLOR = "a {color: #1A78D5; }"
+STYLE_DARK_ABOUT_LINK_COLOR = "color: #1A78D5;"
+STYLE_DARK_ERRORNOTIFICATION = "color: #E94F64;"
+STYLE_DARK_DIFFERENTITEM_COLOR = '#E94F64'
+STYLE_DARK_NOFILEITEM_COLOR = '#1A78D5'
+STYLE_DARK_NOTCONTROLLER_COLOR = 'grey'
+STYLE_DARK_UNTRUSTEDITEM_COLOR = '#882fbc'
+
TLS_CERT_ROTATION_MAX_RETRIES = 10
USERLIST_GUI_USERNAME_OFFSET = getValueForOS({
diff --git a/syncplay/resources/third-party-notices.rtf b/syncplay/resources/third-party-notices.rtf
index 13d3355..207ed83 100644
--- a/syncplay/resources/third-party-notices.rtf
+++ b/syncplay/resources/third-party-notices.rtf
@@ -400,6 +400,35 @@ TIONS OF ANY KIND, either express or implied. See the License for the specific l
uage governing permissions and limitations under the License.\
\
+\b Darkdetect
+\b0 \
+\
+Copyright (c) 2019, Alberto Sottile\
+All rights reserved.\
+\
+Redistribution and use in source and binary forms, with or without\
+modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright\
+ notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright\
+ notice, this list of conditions and the following disclaimer in the\
+ documentation and/or other materials provided with the distribution.\
+ * Neither the name of "darkdetect" nor the\
+ names of its contributors may be used to endorse or promote products\
+ derived from this software without specific prior written permission.\
+\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND\
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\
+DISCLAIMED. IN NO EVENT SHALL "Alberto Sottile" BE LIABLE FOR ANY\
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
+\
+
\b Icons\
\
diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py
index 5f8b17c..de2f6bb 100755
--- a/syncplay/ui/gui.py
+++ b/syncplay/ui/gui.py
@@ -31,6 +31,11 @@ if isMacOS() and IsPySide:
from Foundation import NSURL
from Cocoa import NSString, NSUTF8StringEncoding
lastCheckedForUpdates = None
+from syncplay.vendor import darkdetect
+if isMacOS():
+ isDarkMode = darkdetect.isDark()
+else:
+ isDarkMode = None
class ConsoleInGUI(ConsoleUI):
@@ -51,7 +56,8 @@ class ConsoleInGUI(ConsoleUI):
class UserlistItemDelegate(QtWidgets.QStyledItemDelegate):
- def __init__(self):
+ def __init__(self, view=None):
+ self.view = view
QtWidgets.QStyledItemDelegate.__init__(self)
def sizeHint(self, option, index):
@@ -72,9 +78,10 @@ class UserlistItemDelegate(QtWidgets.QStyledItemDelegate):
roomController = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
userReady = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_READY_ROLE)
isUserRow = indexQModelIndex.parent() != indexQModelIndex.parent().parent()
+ bkgColor = self.view.palette().color(QtGui.QPalette.Base)
if isUserRow and isMacOS():
- whiteRect = QtCore.QRect(0, optionQStyleOptionViewItem.rect.y(), optionQStyleOptionViewItem.rect.width(), optionQStyleOptionViewItem.rect.height())
- itemQPainter.fillRect(whiteRect, QtGui.QColor(Qt.white))
+ blankRect = QtCore.QRect(0, optionQStyleOptionViewItem.rect.y(), optionQStyleOptionViewItem.rect.width(), optionQStyleOptionViewItem.rect.height())
+ itemQPainter.fillRect(blankRect, bkgColor)
if roomController and not controlIconQPixmap.isNull():
itemQPainter.drawPixmap(
@@ -130,7 +137,11 @@ class AboutDialog(QtWidgets.QDialog):
self.setWindowIcon(QtGui.QPixmap(resourcespath + 'syncplay.png'))
nameLabel = QtWidgets.QLabel("
Syncplay")
nameLabel.setFont(QtGui.QFont("Helvetica", 18))
- linkLabel = QtWidgets.QLabel("syncplay.pl")
+ linkLabel = QtWidgets.QLabel()
+ if isDarkMode:
+ linkLabel.setText(("syncplay.pl").format(constants.STYLE_DARK_ABOUT_LINK_COLOR))
+ else:
+ linkLabel.setText("syncplay.pl")
linkLabel.setOpenExternalLinks(True)
versionExtString = version + revision
versionLabel = QtWidgets.QLabel(
@@ -324,11 +335,17 @@ class MainWindow(QtWidgets.QMainWindow):
fileIsAvailable = self.selfWindow.isFileAvailable(itemFilename)
fileIsUntrusted = self.selfWindow.isItemUntrusted(itemFilename)
if fileIsUntrusted:
- self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_UNTRUSTEDITEM_COLOR)))
+ if isDarkMode:
+ self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DARK_UNTRUSTEDITEM_COLOR)))
+ else:
+ self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_UNTRUSTEDITEM_COLOR)))
elif fileIsAvailable:
- self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(QtGui.QPalette.ColorRole(QtGui.QPalette.Text))))
+ self.item(item).setForeground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Text)))
else:
- self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
+ if isDarkMode:
+ self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DARK_DIFFERENTITEM_COLOR)))
+ else:
+ self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
self.selfWindow._syncplayClient.fileSwitch.setFilenameWatchlist(self.selfWindow.newWatchlist)
self.forceUpdate()
@@ -605,24 +622,28 @@ class MainWindow(QtWidgets.QMainWindow):
sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration'])
underlinefont = QtGui.QFont()
underlinefont.setUnderline(True)
+ differentItemColor = constants.STYLE_DARK_DIFFERENTITEM_COLOR if isDarkMode else constants.STYLE_DIFFERENTITEM_COLOR
if sameRoom:
if not sameName:
- filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
+ filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(differentItemColor)))
filenameitem.setFont(underlinefont)
if not sameSize:
if formatSize(user.file['size']) == formatSize(currentUser.file['size']):
filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'], precise=True))
filesizeitem.setFont(underlinefont)
- filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
+ filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(differentItemColor)))
if not sameDuration:
- filedurationitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
+ filedurationitem.setForeground(QtGui.QBrush(QtGui.QColor(differentItemColor)))
filedurationitem.setFont(underlinefont)
else:
filenameitem = QtGui.QStandardItem(getMessage("nofile-note"))
filedurationitem = QtGui.QStandardItem("")
filesizeitem = QtGui.QStandardItem("")
if room == currentUser.room:
- filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR)))
+ if isDarkMode:
+ filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DARK_NOFILEITEM_COLOR)))
+ else:
+ filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR)))
font = QtGui.QFont()
if currentUser.username == user.username:
font.setWeight(QtGui.QFont.Bold)
@@ -637,7 +658,7 @@ class MainWindow(QtWidgets.QMainWindow):
roomitem.appendRow((useritem, filesizeitem, filedurationitem, filenameitem))
self.listTreeModel = self._usertreebuffer
self.listTreeView.setModel(self.listTreeModel)
- self.listTreeView.setItemDelegate(UserlistItemDelegate())
+ self.listTreeView.setItemDelegate(UserlistItemDelegate(view=self.listTreeView))
self.listTreeView.setItemsExpandable(False)
self.listTreeView.setRootIsDecorated(False)
self.listTreeView.expandAll()
@@ -849,7 +870,10 @@ class MainWindow(QtWidgets.QMainWindow):
message = message.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">")
message = message.replace("<a href="https://syncplay.pl/trouble">", '').replace("</a>", "")
message = message.replace("\n", "
")
- message = "".format(constants.STYLE_ERRORNOTIFICATION) + message + ""
+ if isDarkMode:
+ message = "".format(constants.STYLE_DARK_ERRORNOTIFICATION) + message + ""
+ else:
+ message = "".format(constants.STYLE_ERRORNOTIFICATION) + message + ""
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "
")
@needsClient
@@ -1259,6 +1283,7 @@ class MainWindow(QtWidgets.QMainWindow):
window.outputLayout = QtWidgets.QVBoxLayout()
window.outputbox = QtWidgets.QTextBrowser()
+ if isDarkMode: window.outputbox.document().setDefaultStyleSheet(constants.STYLE_DARK_LINKS_COLOR);
window.outputbox.setReadOnly(True)
window.outputbox.setTextInteractionFlags(window.outputbox.textInteractionFlags() | Qt.TextSelectableByKeyboard)
window.outputbox.setOpenExternalLinks(True)
@@ -1266,7 +1291,8 @@ class MainWindow(QtWidgets.QMainWindow):
window.outputbox.moveCursor(QtGui.QTextCursor.End)
window.outputbox.insertHtml(constants.STYLE_CONTACT_INFO.format(getMessage("contact-label")))
window.outputbox.moveCursor(QtGui.QTextCursor.End)
- window.outputbox.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
+ window.outputbox.setCursorWidth(0)
+ if not isMacOS(): window.outputbox.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
window.outputlabel = QtWidgets.QLabel(getMessage("notifications-heading-label"))
window.outputlabel.setMinimumHeight(27)
@@ -1414,8 +1440,6 @@ class MainWindow(QtWidgets.QMainWindow):
playlistItem = QtWidgets.QListWidgetItem(getMessage("playlist-instruction-item-message"))
playlistItem.setFont(noteFont)
window.playlist.addItem(playlistItem)
- playlistItem.setFont(noteFont)
- window.playlist.addItem(playlistItem)
window.playlistLayout.addWidget(window.playlist)
window.playlistLayout.setAlignment(Qt.AlignTop)
window.playlistGroup.setLayout(window.playlistLayout)
diff --git a/syncplay/vendor/darkdetect/__init__.py b/syncplay/vendor/darkdetect/__init__.py
new file mode 100755
index 0000000..0537e58
--- /dev/null
+++ b/syncplay/vendor/darkdetect/__init__.py
@@ -0,0 +1,18 @@
+#-----------------------------------------------------------------------------
+# Copyright (C) 2019 Alberto Sottile
+#
+# Distributed under the terms of the 3-clause BSD License.
+#-----------------------------------------------------------------------------
+
+__version__ = '0.1.0'
+
+import sys
+import platform
+from distutils.version import LooseVersion as V
+
+if sys.platform != "darwin" or V(platform.mac_ver()[0]) < V("10.14"):
+ from ._dummy import *
+else:
+ from ._detect import *
+
+del sys, platform, V
\ No newline at end of file
diff --git a/syncplay/vendor/darkdetect/_detect.py b/syncplay/vendor/darkdetect/_detect.py
new file mode 100755
index 0000000..9a79f7c
--- /dev/null
+++ b/syncplay/vendor/darkdetect/_detect.py
@@ -0,0 +1,64 @@
+#-----------------------------------------------------------------------------
+# Copyright (C) 2019 Alberto Sottile
+#
+# Distributed under the terms of the 3-clause BSD License.
+#-----------------------------------------------------------------------------
+
+import ctypes
+import ctypes.util
+
+appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit'))
+objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc'))
+
+void_p = ctypes.c_void_p
+ull = ctypes.c_uint64
+
+objc.objc_getClass.restype = void_p
+objc.sel_registerName.restype = void_p
+objc.objc_msgSend.restype = void_p
+objc.objc_msgSend.argtypes = [void_p, void_p]
+
+msg = objc.objc_msgSend
+
+def _utf8(s):
+ if not isinstance(s, bytes):
+ s = s.encode('utf8')
+ return s
+
+def n(name):
+ return objc.sel_registerName(_utf8(name))
+
+def C(classname):
+ return objc.objc_getClass(_utf8(classname))
+
+def theme():
+ NSAutoreleasePool = objc.objc_getClass('NSAutoreleasePool')
+ pool = msg(NSAutoreleasePool, n('alloc'))
+ pool = msg(pool, n('init'))
+
+ NSUserDefaults = C('NSUserDefaults')
+ stdUserDef = msg(NSUserDefaults, n('standardUserDefaults'))
+
+ NSString = C('NSString')
+
+ key = msg(NSString, n("stringWithUTF8String:"), _utf8('AppleInterfaceStyle'))
+ appearanceNS = msg(stdUserDef, n('stringForKey:'), void_p(key))
+ appearanceC = msg(appearanceNS, n('UTF8String'))
+
+ if appearanceC is not None:
+ out = ctypes.string_at(appearanceC)
+ else:
+ out = None
+
+ msg(pool, n('release'))
+
+ if out is not None:
+ return out.decode('utf-8')
+ else:
+ return 'Light'
+
+def isDark():
+ return theme() == 'Dark'
+
+def isLight():
+ return theme() == 'Light'
diff --git a/syncplay/vendor/darkdetect/_dummy.py b/syncplay/vendor/darkdetect/_dummy.py
new file mode 100755
index 0000000..1e99668
--- /dev/null
+++ b/syncplay/vendor/darkdetect/_dummy.py
@@ -0,0 +1,14 @@
+#-----------------------------------------------------------------------------
+# Copyright (C) 2019 Alberto Sottile
+#
+# Distributed under the terms of the 3-clause BSD License.
+#-----------------------------------------------------------------------------
+
+def theme():
+ return None
+
+def isDark():
+ return None
+
+def isLight():
+ return None