mirror of https://github.com/Syncplay/syncplay
macOS: add support for Mojave Dark Mode
This commit adds to our UI the support for macOS 10.14+ Dark Mode. Qt already adapts a large fraction of the color scheme, but some label colors had to be adjusted and were put in separate STYLE_DARK constants. To determine if the OS is set in Dark or Light Mode, we use a new dependency, included in the vendor folder: Darkdetect - license: BSD-3-Clause To allow the app bundle to use the Dark Mode APIs, a constant is added in the info.plist ('NSRequiresAquaSystemAppearance': False)
This commit is contained in:
parent
12fc04326a
commit
bc242c2565
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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\
|
||||
\
|
||||
|
||||
|
|
|
@ -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("<center><strong>Syncplay</strong></center>")
|
||||
nameLabel.setFont(QtGui.QFont("Helvetica", 18))
|
||||
linkLabel = QtWidgets.QLabel("<center><a href=\"https://syncplay.pl\">syncplay.pl</a></center>")
|
||||
linkLabel = QtWidgets.QLabel()
|
||||
if isDarkMode:
|
||||
linkLabel.setText(("<center><a href=\"https://syncplay.pl\" style=\"{}\">syncplay.pl</a></center>").format(constants.STYLE_DARK_ABOUT_LINK_COLOR))
|
||||
else:
|
||||
linkLabel.setText("<center><a href=\"https://syncplay.pl\">syncplay.pl</a></center>")
|
||||
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">", '<a href="https://syncplay.pl/trouble">').replace("</a>", "</a>")
|
||||
message = message.replace("\n", "<br />")
|
||||
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
|
||||
if isDarkMode:
|
||||
message = "<span style=\"{}\">".format(constants.STYLE_DARK_ERRORNOTIFICATION) + message + "</span>"
|
||||
else:
|
||||
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
|
||||
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />")
|
||||
|
||||
@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)
|
||||
|
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
Loading…
Reference in New Issue