TweetFollow Us on Twitter

Horse Feathers

Volume Number: 20 (2004)
Issue Number: 7
Column Tag: Programming

QuickTime Toolkit

by Tim Monroe

Horse Feathers

Introduction

In the previous article ("Tickle Me" in MacTech, June 2004), we took a look at using the Tcl scripting language and the Tk graphical user interface toolkit to build a QuickTime application. We saw how to use the QuickTimeTcl extension to add to a basic Tcl/Tk application the ability to open and display QuickTime movies. Figure 1 shows a movie displayed by our sample application, called TickLeez.


Figure 1: A movie window displayed by TickLeez (Macintosh)

In this article, I want to continue investigating QuickTimeTcl. In particular, we'll see how to configure our application to react to keyboard and operating system events. We'll also see how to implement movie editing. By the end of this article, TickLeez will incorporate all the standard movie playback and editing behaviors.

Event Bindings

Last time, we saw how to create the menu bar and menus for the TickLeez application. Later in this article, we'll learn how to adjust the state of menu items according to the current edited state of the frontmost movie window. Right now, we need to create some Tk bindings for a handful of events.

An event binding attaches a Tcl command to a specific event related to a movie window or to some other event, such as a keyboard event. The -accelerator options we specified when creating the menu items affect only the appearance of the menu items. To enable the keyboard shortcuts, we need to call the bind command, for instance like this:

bind .$winName <$appData(modKey)-n> {newDoc}

Tk also allows us to create bindings for activate and deactivate events for a window and for mouse-enter and mouse-exit events. Our complete set of event bindings is established in the setupBindings procedure, shown in Listing 1.

Listing 1: Setting up key bindings

setupBindings
proc setupBindings {winName} {
   global appData
   bind .$winName <$appData(modKey)-n> {newDoc}
   bind .$winName <$appData(modKey)-o> {openDoc}
   bind .$winName <$appData(modKey)-q> {exit}
      
   if {[isMovieWindow $winName]} {
      # window state-change bindings
      bind .$winName <Activate> {adjustMenus}
      bind .$winName <Deactivate> {adjustMenus}
      
      # accelerator key bindings
      bind .$winName <$appData(modKey)-w> {closeDoc}
      bind .$winName <$appData(modKey)-s> {saveDoc}
      bind .$winName <$appData(modKey)-S> {saveAsDoc}
      
      bind .$winName <$appData(modKey)-z> {undoDoc}
      bind .$winName <$appData(modKey)-x> {editDoc cut}
      bind .$winName <$appData(modKey)-c> {editDoc copy}
      bind .$winName <$appData(modKey)-v> {editDoc paste}
      bind .$winName <$appData(modKey)-a> {selectDoc all}
      bind .$winName <$appData(modKey)-b> {selectDoc none}
      
      bind .$winName <$appData(modKey)-KeyPress-1> \
                                                {puts "TO BE PROVIDED"}
      
      # miscellaneous bindings
      bind .$winName <Delete> {editDoc cut}
      bind .$winName <KeyPress> "handleKey $winName %K"
      bind .$winName <Leave> ".$winName configure \
                                                -cursor arrow"
   }
}

Note the Leave binding: it's mainly intended for QuickTime VR movies, where the cursor is not automatically changed to the normal arrow cursor when it moves outside of the movie rectangle. (Other media types that alter the cursor, such as Flash, do not have this problem, so one could reasonably consider the VR behavior to be a bug.) Keep in mind that this workaround is not perfect: the cursor does not change to an arrow until the cursor hotspot has completely exited the movie window. This means that it will remain as the current VR cursor if moved into the window's title bar. Addressing that issue is left as an exercise for the reader.

Movie Playback

Once we've loaded a movie into a movie window (using the configure command), QuickTimeTcl takes care of all the nitty-gritty details of passing user or system events to the movie and making sure it reacts appropriately to those events. Mouse events on the movie controller bar work fine. Moreover, QuickTimeTcl takes care of tasking the movie often enough to ensure uninterrupted playback of linear movies or smooth user interaction with nonlinear movies like QuickTime VR movies or wired sprite movies.

Handling Keyboard Events

QuickTimeTcl, however, does not appear to provide built-in support for the standard methods of controlling movie playback using the keyboard. With a linear movie, for instance, pressing the space bar should toggle the current play state of the movie. And with a QuickTime VR movie, pressing the left arrow key should pan the movie to the left.

As you've probably guessed, we can support these behaviors by creating the appropriate event bindings. The setupBindings function considered just above binds key presses to the handleKey function, like so:

bind .$winName <KeyPress> "handleKey $winName %K"

The "%K" keyword is replaced with the keysym, which is a special Tk designator for a key on the keyboard. For instance, the keysym for the space bar is "space", and the keysym for the Shift key is "Shift_L".

Our implementation of handleKey, shown in Listing 2, first determines whether the specified movie window is a QuickTime VR movie or not. (The command ispanoramic is slightly misnamed, as it also returns 1 for a QuickTime VR object movie.) For a QuickTime VR movie, we use the movie widget commands pan, tilt, and fieldofview to adjust the view parameters. For linear movies, we use the commands step, play, and stop to adjust the movie time and play rate. We also adjust the movie's volume when we receive an up arrow or down arrow key.

Listing 2: Handling key presses

handleKey
proc handleKey {winName key} {
   global appData
    
   set movie $appData($winName,movie)
   if {[$movie ispanoramic]} {
      # handle QTVR movie controller key bindings 
      # (and, yes, "ispanoramic" is true for object movies too)
      switch $key {
         "Right" {$movie pan [expr [$movie pan] - 10]}
         "Left" {$movie pan [expr [$movie pan] + 10]}
         "Up" {$movie tilt [expr [$movie tilt] + 10]}
         "Down" {$movie tilt [expr [$movie tilt] - 10]}
         "Shift_L" {$movie fieldofview [expr [$movie \
                              fieldofview] - 5]}
         "Control_L" {$movie fieldofview [expr [$movie \
                              fieldofview] + 5]}
      }
   } else {
      # handle standard movie controller key bindings
      switch $key {
         "Right" {$movie step 1}
         "Left" {$movie step -1}
         "Up" {$movie configure -volume [min 255 \
                  [expr 64 + [$movie cget -volume]]]}
         "Down" {$movie configure -volume [max 0 \
                  [expr -64 + [$movie cget -volume]]]}
         "space" {if {[$movie rate] == 0} {$movie play} 
                                                else {$movie stop}}
      }
   }
}

Installing a Movie Controller Callback Function

At this point, TickLeez can open and display QuickTime movies, and it can handle all the standard means of user interaction with the movie. Before we move on to consider how to support movie editing, I should mention that QuickTimeTcl provides a way to observe and possibly override actions associated with the movie; that is to say, it provides a wrapper for QuickTime's movie controller action filter function.

We installed this movie controller callback function when we configured our movie widget, by passing a procedure name to the -mccommand argument:

.$winName.mov configure -mccommand controllerProc \
               -mcedit 1 -resizable 1

The specified procedure is passed the name of the movie widget, a string representing the action, and a possibly empty list of parameters associated with that action. For linear movies, these actions include activate, customButtonClick, deactivate, key, goToTime, mouseDown, play, setSelectionBegin, setSelectionDuration, and setVolume. For QuickTime VR movies, these actions also include pan, tilt, fieldofview, and triggerhotspot. Listing 3 shows the basic form of a movie controller callback function.

Listing 3: Handling movie controller actions

controllerProc
proc controllerProc {w what {par {}}} {
   if {![isMovieWindow [string range $w 1 end]]} {
      return
   }
   if {$what == "key"} {
      puts "got key event in controller action proc"
   }
   
}

Normally the action is processed by the movie controller after our callback procedure returns. To suppress an action, we can execute this line of code:

return -code 3

Movie Editing

QuickTimeTcl provides a number of movie widget commands for performing the standard movie editing operations (cut, copy, paste, and so forth). It also provides support for multilevel undo, that is, the ability to undo more than one previous editing operation. None of the QuickTime-savvy applications that we've built hitherto supports this very nice feature, except for the MooVeez application we built in an earlier article, where the multilevel undo is provided by the Cocoa application framework. (See "The Cocoanuts" in MacTech, December 2002.) In this section, we'll see how to implement editing in TickLeez and how to adjust the File and Edit menus in accordance with the edited state of the frontmost movie window.

Handling Editing Operations

We've already seen how to invoke an edit command when the appropriate menu item is selected. For instance, in the setupMenus procedure (Listing 4 in the previous article), we saw this line of code for setting up the Clear menu item:

$m add command -label "Clear" -command {editDoc clear}

So, when the user selects the Clear menu item, the editDoc procedure is called with the "clear" parameter. Listing 4 shows our implementation of the editDoc procedure.

Listing 4: Editing a movie

editDoc
proc editDoc {op} {
   global appData
   set winName [topMovieWindow]
   $appData($winName,movie) $op
   if {$op != "copy"} {
      set n [incr appData($winName,undoLevel)]
      set appData($winName,undoOp,$n) $op
      setWindowDirty $winName 1 
   }
   adjustSize $winName
   adjustMenus
}

The key step here is the line of code that sends the specified operation string $op to the frontmost movie widget as a command. The movie widget supports these editing commands: cut, copy, paste, clear, add, and undo. Notice that we also increment the widget's undo level and store the operation string itself in the movie window's global data array. In a moment, we'll see how these items are used to support multilevel undo.

The editDoc function also calls the setWindowDirty function (shown in Listing 5) to mark the movie window as having been changed. (Note that QuickTimeTcl provides a built-in procedure haschanged that we could use instead of reading this stored value; however, I prefer to keep track of changes to the movie myself.)

Listing 5: Marking a movie window as changed

setWindowDirty
proc setWindowDirty {winName state} {
   global appData
   set appData($winName,dirty) $state
   if {[string match "mac*" $appData(os)]} {
      wm attributes .$winName -modified $state
   }
}

Notice that, on Macintosh computers, we also set the modified attribute of the movie window; on Mac OS X, this has the effect of drawing a dot inside the window's close button, as shown in Figure 2.


Figure 2: A modified movie window

Undoing Editing Operations

Whenever an editing operation is performed, QuickTimeTcl adds information about the state of the movie just prior to the edit to an edit stack. The number of the topmost item on the stack is called the undo level. When a movie is first opened, its undo level is 0. Subsequent editing operations increment the undo level.

QuickTimeTcl allows us to undo one or more edits by executing the undo command. This command requires one parameter, which specifies the undo level we want to revert to. In TickLeez, we shall always revert to the previous edit, so we'll decrement the undo level by one and pass that number to undo. This is how we implement multilevel undo. Listing 6 shows our definition of the undoDoc function.

Listing 6: Undoing an edit

undoDoc
proc undoDoc {} {
   global appData
   set winName [topMovieWindow]
   if {$appData($winName,undoLevel) > 0} {
      incr appData($winName,undoLevel) -1
      $appData($winName,movie) undo \
                                       $appData($winName,undoLevel)
   }
   
   if {$appData($winName,undoLevel) == 0} {
      setWindowDirty $winName 0 
   }
   
   adjustMenus
}

As you can see, if the user undoes all edits (so that the undo level reaches 0 once again), we call setWindowDirty with the parameter 0 to mark the window as not dirty.

Selecting All or None of a Movie

The Edit menu in TickLeez contains the Select All and Select None menu items. In previous articles, we've seen how to handle those items quite easily. For instance, we can handle the Select All item by setting the beginning of the movie selection to movie time 0 and the duration of the movie selection to the duration of the entire movie.

QuickTimeTcl provides the select command that we can use to set the movie selection to a specified range, but it does not provide a command that returns the duration of a movie directly. Instead, QuickTimeTcl provides the gettime command, which returns an array of four movie-related times: the current time, the length of the movie, the movie time scale, and the movie poster time. For a specific movie window, we can retrieve that array like this:

array set timeArr [$appData($winName,movie) gettime]

Once we've successfully done that, we can read the movie duration by looking at the $timeArr(-movieduration) element in the array; and we can read the current movie time by looking at the $timeArr(-movietime) element. Then we can handle the two selection menu items using the selectDoc function, defined in Listing 7.

Listing 7: Selecting all or none of a movie

selectDoc
proc selectDoc {op} {
   global appData
   set winName [topMovieWindow]
   
   array set timeArr [$appData($winName,movie) gettime]
   switch -- ${op} {
       all {$appData($winName,movie) select \
               0 $timeArr(-movieduration)}
       none {$appData($winName,movie) select \
               $timeArr(-movietime) 0}
   }
}

Enabling and Disabling Menu Items

As you know, we need to adjust some of the items in the File and Edit menus based on the current state of the frontmost movie window (if one exists). For instance, if a movie has been edited, we need to enable the Save menu item in the File menu so that the user can save those changes, if desired. Listing 8 shows our implementation of the adjustMenus function, which is called by many of the editing functions we've encountered recently. We handle the Save menu item by looking at the dirty field of the stored movie window data, like this:

if {$appData($winName,dirty) == 1} {
   $bar.file entryconfigure "Save" -state active}

Here, we use Tk's entryconfigure command to set the state of a menu item.

We can determine whether other menu items should be enabled or disabled by executing the controllerinfo command. Like the gettime command we just considered, controllerinfo fills an array with a set of entries, in this case with entries that indicate the current state of the movie controller.

array set infoArr [$appData($winName,movie) controllerinfo]

For instance, if the $infoArr(-cutavailable) item is 1, then we want to enable the Cut menu item.

Listing 8: Adjusting the menus and menu items

adjustMenus
proc adjustMenus {} {
   global appData
   
   set winName [topMovieWindow]
   set bar .${winName}mbar
   if {[string equal $appData(os) "windows"]} {
      if {[string equal $winName ""]} {
         return
      }
   }
   # set the default state
   $bar.file entryconfigure "New" -state active
   $bar.file entryconfigure "Open..." -state active
   $bar.file entryconfigure "Close" -state disabled
   $bar.file entryconfigure "Save" -state disabled
   $bar.file entryconfigure "Save As..." -state disabled
   
   $bar.edit entryconfigure "Undo" -state disabled
   $bar.edit entryconfigure "Cut" -state disabled
   $bar.edit entryconfigure "Copy" -state disabled
   $bar.edit entryconfigure "Paste" -state disabled
   $bar.edit entryconfigure "Clear" -state disabled
   $bar.edit entryconfigure "Select All" -state disabled
   $bar.edit entryconfigure "Select None" -state disabled
   
   $bar.movie entryconfigure "Hide Controller Bar" \
            -state disabled
   if {[string equal $appData(os) "windows"]} {
      $bar.help entryconfigure "About TickLeez" -state active
   } else {
      $bar.apple entryconfigure "About TickLeez" \
            -state active
   }
   
   if {[isMovieWindow $winName]} {
      $bar.file entryconfigure "Close" -state active
      $bar.file entryconfigure "Save As..." -state active
      $bar.edit entryconfigure "Select All" -state active
      $bar.edit entryconfigure "Select None" -state active
   
      if {$appData($winName,dirty) == 1} {
         $bar.file entryconfigure "Save" -state active}
      
      array set infoArr [$appData($winName,movie) \
                                                         controllerinfo]
      if {$appData($winName,undoLevel) > 0} {
         $bar.edit entryconfigure "Undo" -state active}
      if {$infoArr(-cutavailable) == 1} {
         $bar.edit entryconfigure "Cut" -state active}
      if {$infoArr(-copyavailable) == 1} {
         $bar.edit entryconfigure "Copy" -state active}
      if {$infoArr(-pasteavailable) == 1} {
         $bar.edit entryconfigure "Paste" -state active}
      if {$infoArr(-clearavailable) == 1} {
         $bar.edit entryconfigure "Clear" -state active}
      if {$appData($winName,undoLevel) > 0} {
         set op \ $appData($winName,undoOp,$appData($winName,undoLevel))
         $bar.edit entryconfigure "Undo*" -label \
                                    "Undo [string totitle $op]"
      }
      
      $bar.movie entryconfigure "Hide Controller Bar" \
            -state active
   }
}

File Manipulation

Our final task before wrapping up TickLeez for delivery to end users is to handle the document-related operations -- that is saving an edited movie, saving a movie into a new file, closing a movie window, and ultimately quitting the application itself. As we've seen in previous articles, we need to track the state of a movie and prompt the user to save or discard changes to it when the movie window is to be closed or the application is to be quit.

Closing a Movie Window

When the user selects the Close menu item in the File menu (or types the appropriate keyboard shortcut), TickLeez executes the closeDoc procedure, which is defined in Listing 9.

Listing 9: Handling the Close menu item

closeDoc
proc closeDoc {} {
   attemptClose [topMovieWindow] closing
}

As you can see, closeDoc finds the name of the frontmost movie window and passes that name as an argument to the attemptClose function, along with the action name "closing". The attemptClose function needs to look at the dirty state of the window and, if necessary, display a dialog box asking the user to save or discard the changes to the movie. Listing 10 shows our definition of attemptClose.

Listing 10: Prompting the user to save a changed movie

attemptClose
proc attemptClose {winName action} {
   global appData
   set closeWindow 1
   if {$appData($winName,dirty) == 1} {
      set movieName [file tail $appData($winName,fileName)]
   
      set answer [tk_messageBox \
         -parent .$winName \
         -title "Save changes before $action" \
         -message "Do you want to save the changes you made \
                     to the document \u201C$movieName\u201D \
                     before $action?" \
         -type yesnocancel \
         -icon warning]
   
      switch $answer {
         yes         {set closeWindow [saveDoc]}
         cancel   {set closeWindow 0}
         no          {set closeWindow 1}
      }
   }
   if {$closeWindow == 1} {disposeDoc $winName}
   return $closeWindow
}

We display the Save Changes dialog box by calling the tk_messageBox function, this time with the yesnocancel type. Figure 3 shows the Macintosh dialog box, and Figure 4 shows the Windows dialog box.


Figure 3: The Save Changes dialog box of TickLeez (Macintosh)


Figure 4: The Save Changes dialog box of TickLeez (Windows)

Notice in Listing 10 that if the user selects Yes, we call the saveDoc procedure (defined later) and then set the closeWindow variable to the value it returns. We set the closeWindow variable to 1 if the user selects No and to 0 if the user selects Cancel. If closeWindow is 1, we call the disposeDoc function, defined in Listing 11.

Listing 11: Disposing a movie window and its associated data

disposeDoc
proc disposeDoc {winName} {
   global appData
   
   if {![isMovieWindow $winName]} {return}
   destroy .$winName
   array unset appData $winName,*
   adjustMenus
}

The key step in disposeDoc is to call the Tk function destroy, which removes the specified window from the screen and dispose of any memory it occupies. We also call the array unset command to remove from the appData associative array any elements associated with the movie window.

Saving a Changed Movie

As we've seen, TickLeez calls the saveDoc function when the user selects the Save menu item (or types the appropriate keyboard equivalent). In addition, the attemptClose method (Listing 10) calls the saveDoc function if the user elects to save the changes to a window that is about to close. It's easy to implement saveDoc, using the save command provided by QuickTimeTcl, as shown in Listing 12.

Listing 12: Saving a movie

saveDoc
proc saveDoc {} {
   global appData
   set winName [topMovieWindow]
   # if the movie file is in our temporary directory, alert the user
   if {[string compare \
      -length [string length $appData(tempDir)] \
      $appData($winName,fileName) $appData(tempDir)] == 0} {
      tk_messageBox \
         -parent .$winName \
         -title "Warning" \
         -message "Cannot save this movie into the current \
            folder.\nPlease choose \u201CSave As...\u201D to \
            select a different folder." \
         -type ok \
         -icon warning
      return    0
   }
   
   $appData($winName,movie) save
   
   setWindowDirty $winName 0 
   adjustMenus
   return 1
}

Recall that QuickTimeTcl requires a filename for each open movie and that we create a temporary file when the user selects the New command in the File menu. If the file we are saving is located in the designated temporary directory, we want to display the alert sheet shown in Figure 5 to force the user to save the file elsewhere.


Figure 5: The bad directory alert sheet

Saving a Movie into a New File

When the user selects the Save As menu item in the File menu, we need to elicit a location for the new movie file and then save the movie into that file. In TickLeez, we'll execute this line of code:

set newFile [tk_getSaveFile]

The tk_getSaveFile function displays a dialog box like the one shown in Figure 6. It returns as its result the full pathname of the specified file, or an empty string if the user did not specify a file.


Figure 6: The file-saving dialog box displayed by Tk

If the user does specify a file, we can write the movie into that file by calling the QuickTimeTcl command flatten, as shown in Listing 13

Listing 13: Saving a movie into a new file

saveAsDoc
proc saveAsDoc {} {
   global appData
   set winName [topMovieWindow]
   set newFile [tk_getSaveFile]
   if {$newFile != ""} {
      $appData($winName,movie) flatten $newFile
      $appData($winName,movie) configure -file $newFile
      
      wm title .$winName [file tail $newFile]
      
      set appData($winName,dirty) 0   
      set appData($winName,fileName) $newFile   
      set appData($winName,undoLevel) 0
      
      setWindowDirty $winName 0 
   }
   
   adjustMenus
}

As you know, the standard behavior of a Save As operation is for the new movie to replace the existing movie in the existing movie window. Accordingly, saveAsDoc calls the QuickTimeTcl command configure with the full pathname of the new movie file. Then it resets the window title and several fields of the global data array associated with the movie.

Quitting the Application

When the user decides to quit TickLeez, we need to perform the standard quitting-time operations (such as making sure the user saves or discards any unsaved changes to the movie windows). When the user selects the Quit menu item, Tcl calls its own exit function, which performs any Tcl-specific cleanup tasks. We can make sure that our own cleanup tasks are performed prior to that by renaming the exit function to some other name, like this:

rename exit __exit

Then we can define our own exit function, as shown in Listing 14.

Listing 14: Quitting the application

exit
proc exit {} {
   quitApp
}

The TickLeez function quitApp is shown in Listing 15. After we've inspected all movie windows and made sure that all changes have been saved, we remove the temporary directory we created earlier. Then we call __exit to let Tcl perform its cleanup tasks.

Listing 15: Handling the Quit menu item

quitApp
proc quitApp {} {
   global appData
   set cancelled 0
   
   while {([topMovieWindow] != "") && ($cancelled == 0)} {
      set cancelled [expr ![attemptClose [topMovieWindow] \
               quitting]]
   }
   if {$cancelled == 0} {
      # remove the temp directory if it exists
      if {[file exists $appData(tempDir)]} {
         file delete -force $appData(tempDir)
      }
      __exit
   }
}

Conclusion

This brings us to the end of our investigation of Tcl/Tk and QuickTimeTcl as a delivery vehicle for QuickTime applications. In just under four hundred lines of script (not counting the blank lines and comments in the file TickLeez.tcl), we've managed to construct a multi-window QuickTime playback and editing application that runs on both Macintosh and Windows computers. TickLeez exhibits all the standard user-interaction and document-related behaviors that we've come to expect of a QuickTime application, and it does so with a minimum of platform-specific code. I think that QuickTimeTcl provides a very nice wrapper for the underlying QuickTime APIs. Better still, its source code is freely available and can be easily modified to add capabilities that it does not currently provide. Neither Tcl/Tk nor QuickTimeTcl is perfect, but all three packages are under active development and promise to get even better as time goes by.

Acknowledgements

Thanks are due once again to Jim Ingham and to Mats Bengtsson for reviewing a draft of this article and for providing invaluable feedback.


Tim Monroe is a member of the QuickTime engineering team at Apple. You can contact him at monroe@mactech.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Call of Duty Warzone is a Waiting Simula...
It's always fun when a splashy multiplayer game comes to mobile because they are few and far between, so I was excited to see the notification about Call of Duty: Warzone Mobile (finally) launching last week and wanted to try it out. As someone who... | Read more »
Albion Online introduces some massive ne...
Sandbox Interactive has announced an upcoming update to its flagship MMORPG Albion Online, containing massive updates to its existing guild Vs guild systems. Someone clearly rewatched the Helms Deep battle in Lord of the Rings and spent the next... | Read more »
Chucklefish announces launch date of the...
Chucklefish, the indie London-based team we probably all know from developing Terraria or their stint publishing Stardew Valley, has revealed the mobile release date for roguelike deck-builder Wildfrost. Developed by Gaziter and Deadpan Games, the... | Read more »
Netmarble opens pre-registration for act...
It has been close to three years since Netmarble announced they would be adapting the smash series Solo Leveling into a video game, and at last, they have announced the opening of pre-orders for Solo Leveling: Arise. [Read more] | Read more »
PUBG Mobile celebrates sixth anniversary...
For the past six years, PUBG Mobile has been one of the most popular shooters you can play in the palm of your hand, and Krafton is celebrating this milestone and many years of ups by teaming up with hit music man JVKE to create a special song for... | Read more »
ASTRA: Knights of Veda refuse to pump th...
In perhaps the most recent example of being incredibly eager, ASTRA: Knights of Veda has dropped its second collaboration with South Korean boyband Seventeen, named so as it consists of exactly thirteen members and a video collaboration with Lee... | Read more »
Collect all your cats and caterpillars a...
If you are growing tired of trying to build a town with your phone by using it as a tiny, ineffectual shover then fear no longer, as Independent Arts Software has announced the upcoming release of Construction Simulator 4, from the critically... | Read more »
Backbone complete its lineup of 2nd Gene...
With all the ports of big AAA games that have been coming to mobile, it is becoming more convenient than ever to own a good controller, and to help with this Backbone has announced the completion of their 2nd generation product lineup with their... | Read more »
Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »
Live, Playdate, Live! – The TouchArcade...
In this week’s episode of The TouchArcade Show we kick things off by talking about all the games I splurged on during the recent Playdate Catalog one-year anniversary sale, including the new Lucas Pope jam Mars After Midnight. We haven’t played any... | Read more »

Price Scanner via MacPrices.net

Deal Alert! B&H Photo has Apple’s 14-inch...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on sale for $200-$300 off MSRP, starting at only $1399. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8... Read more
Department Of Justice Sets Sights On Apple In...
NEWS – The ball has finally dropped on the big Apple. The ball (metaphorically speaking) — an antitrust lawsuit filed in the U.S. on March 21 by the Department of Justice (DOJ) — came down following... Read more
New 13-inch M3 MacBook Air on sale for $999,...
Amazon has Apple’s new 13″ M3 MacBook Air on sale for $100 off MSRP for the first time, now just $999 shipped. Shipping is free: – 13″ MacBook Air (8GB RAM/256GB SSD/Space Gray): $999 $100 off MSRP... Read more
Amazon has Apple’s 9th-generation WiFi iPads...
Amazon has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80-$100 off MSRP, starting only $249. Their prices are the lowest available for new iPads anywhere: – 10″ 64GB WiFi iPad (Space Gray or... Read more
Discounted 14-inch M3 MacBook Pros with 16GB...
Apple retailer Expercom has 14″ MacBook Pros with M3 CPUs and 16GB of standard memory discounted by up to $120 off Apple’s MSRP: – 14″ M3 MacBook Pro (16GB RAM/256GB SSD): $1691.06 $108 off MSRP – 14... Read more
Clearance 15-inch M2 MacBook Airs on sale for...
B&H Photo has Apple’s 15″ MacBook Airs with M2 CPUs (8GB RAM/256GB SSD) in stock today and on clearance sale for $999 in all four colors. Free 1-2 delivery is available to most US addresses.... Read more
Clearance 13-inch M1 MacBook Airs drop to onl...
B&H has Apple’s base 13″ M1 MacBook Air (Space Gray, Silver, & Gold) in stock and on clearance sale today for $300 off MSRP, only $699. Free 1-2 day shipping is available to most addresses in... Read more
New promo at Visible: Buy a new iPhone, get $...
Switch to Visible, and buy a new iPhone, and Visible will take $10 off their monthly Visible+ service for 24 months. Visible+ is normally $45 per month. With this promotion, the cost of Visible+ is... Read more
B&H has Apple’s 13-inch M2 MacBook Airs o...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock and on sale for $100 off Apple’s new MSRP, only $899. Free 1-2 day delivery is available to most US addresses. Their... Read more
Take advantage of Apple’s steep discounts on...
Apple has a full line of 16″ M3 Pro and M3 Max MacBook Pros available, Certified Refurbished, starting at $2119 and ranging up to $600 off MSRP. Each model features a new outer case, shipping is free... Read more

Jobs Board

Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Business Analyst | *Apple* Pay - Banco Popu...
Business Analyst | Apple PayApply now " Apply now + Apply Now + Start applying with LinkedIn Start + Please wait Date:Mar 19, 2024 Location: San Juan-Cupey, PR Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.